經過一段時間的開發與測驗,終于發布了Lms框架的第一個正式版本(1.0.0版本),并給出了lms框架的樣例專案lms.samples,本文通過對lms.samples的介紹,簡述如何通過lms框架快速的構建一個微服務的業務框架,并進行應用開發,
lms.samples專案基本介紹
lms.sample專案由三個獨立的微服務應用模塊組成:account、stock、order和一個網關專案gateway構成,
業務應用模塊
每個獨立的微服務應用采用模塊化設計,主要由如下幾部分組成:
-
主機(Host): 主要用于托管微服務應用本身,主機通過參考應用服務專案(應用介面的實作),托管微服務應用,通過托管應用服務,在主機啟動的程序中,向服務注冊中心注冊服務路由,
-
應用介面層(Application.Contracts): 用于定義應用服務介面,通過應用介面,該微服務模塊與其他微服務模塊或是網關進行rpc通信的能力,在該專案中,除了定義應用服務介面之前,一般還定義與該應用介面相關的
DTO物件,應用介面除了被該微服務應用專案參考,并實作應用服務之前,還可以被網關或是其他微服務模塊參考,網關或是其他微服務專案通過應用介面生成的代理與該微服務模塊通過rpc進行通信, -
應用服務層(Application): 應用服務是該微服務定義的應用介面的實作,應用服務與DDD傳統分層架構的應用層的概念一致,主要負責外部通信與領域層之間的協調,一般地,應用服務進行業務流程控制,但是不包含業務邏輯的實作,
-
領域層(Domain): 負責表達業務概念,業務狀態資訊以及業務規則,是該微服務模塊的業務核心,一般地,在該層可以定義聚合根、物體、領域服務等物件,
-
領域共享層(Domain.Shared): 該層用于定義與領域物件相關的模型、物體等相關型別,不包含任何業務實作,可以被其他微服務參考,
-
資料訪問(DataAccess)層: 該層一般用于封裝資料訪問相關的物件,例如:倉庫物件、
SqlHelper、或是ORM相關的型別等,在lms.samples中,通過efcore實作資料的讀寫操作,
(
)
服務聚合與網關
lms框架不允許服務外部與微服務主機直接通信,應用請求必須通過http請求到達網關,網關通過lms提供的中間件決議到服務條目,并通過rpc與集群內部的微服務進行通信,所以,如果服務需要與集群外部進行通信,那么,開發者定義的網關必須要參考各個微服務模塊的應用介面層;以及必須要使用lms相關的中間件,
開發環境
-
.net版本: 5.0.101
-
lms版本: 1.0.0
-
IDE: (1) visual studio 最新版 (2) Rider(推薦)
主機與應用托管
主機的創建步驟
通過lms框架創建一個業務模塊非常方便,只需要通過如下4個步驟,就可以輕松的創建一個lms應用業務模塊,
- 創建專案
創建控制臺應用(Console Application)專案,并且參考Silky.Lms.NormHost包,
dotnet add package Silky.Lms.NormHost --version 1.0.0
- 應用程式入口與主機構建
在main方法中,通用.net的主機Host構建并注冊lms微服務,在注冊lms微服務時,需要指定lms啟動的依賴模塊,
一般地,如果開發者不需要額外依賴其他模塊,也無需在應用啟動或停止時執行方法,那么您可以直接指定NormHostModule模塊,
public class Program
{
public static async Task Main(string[] args)
{
await CreateHostBuilder(args).Build().RunAsync();
}
private static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.RegisterLmsServices<NormHostModule>()
;
}
}
- 組態檔
lms框架支持yml或是json格式作為組態檔,通過appsettings.yml對lms框架進行統一配置,通過appsettings.${Environment}.yml對不同環境變數下的配置項進行設定,
開發者如果直接通過專案的方式啟動應用,那么可以通過Properties/launchSettings.json的environmentVariables.DOTNET_ENVIRONMENT環境變數,如果通過docker-compose的方式啟動應用,那么可以通過.env設定DOTNET_ENVIRONMENT環境變數,
為保證組態檔有效,開發者需要顯式的將組態檔拷貝到專案生成目錄下,
- 參考應用服務層和資料訪問層
一般地,主機專案需要參考該微服務模塊的應用服務層和資料訪問層,只有主機參考應用服務層,主機在啟動時,才會生成服務條目的路由,并且將服務路由注冊到服務注冊中心,
一個典型的主機專案檔案如下所示:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Silky.Lms.NormHost" Version="$(LmsVersion)" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.Production.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.Development.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lms.Account.Application\Lms.Account.Application.csproj" />
<ProjectReference Include="..\Lms.Account.EntityFrameworkCore\Lms.Account.EntityFrameworkCore.csproj" />
</ItemGroup>
</Project>
配置
一般地,一個微服務模塊的主機必須要配置:服務注冊中心、分布式鎖鏈接、分布式快取地址、集群rpc通信token、資料庫鏈接地址等,
如果使用docker-compose來啟動和除錯應用的話,那么,rpc配置節點下的的host和port可以預設,因為生成的每個容器的都有自己的地址和埠號,
如果直接通過專案的方式啟動和除錯應用的話,那么,必須要配置rpc節點下的port,每個微服務模塊的主機應用有自己的埠號,
lms框架的必要配置如下所示:
rpc:
host: 0.0.0.0
rpcPort: 2201
token: ypjdYOzNd4FwENJiEARMLWwK0v7QUHPW
registrycenter:
connectionStrings: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183;127.0.0.1:2184,127.0.0.1:2185,127.0.0.1:2186 # 使用分號;來區分不同的服務注冊中心
registryCenterType: Zookeeper
distributedCache:
redis:
isEnabled: true
configuration: 127.0.0.1:6379,defaultDatabase=0
lock:
lockRedisConnection: 127.0.0.1:6379,defaultDatabase=1
connectionStrings:
default: server=127.0.0.1;port=3306;database=account;uid=root;pwd=qwe!P4ss;
應用介面
應用介面定義
一般地,在應用介面層開發者需要安裝Silky.Lms.Rpc包,如果該微服務模塊還涉及到分布式事務,那么還需要安裝Silky.Lms.Transaction.Tcc,當然,您也可以選擇在應用介面層安裝Silky.Lms.Transaction包,在應用服務層安裝Silky.Lms.Transaction.Tcc包,
-
開發者只需要在應用介面通過
ServiceRouteAttribute特性對應用介面進行直接即可, -
Lms約定應用介面應當以
IXxxAppService命名,這樣,服務條目生成的路由則會以api/xxx形式生成,當然這并不是強制的, -
每個應用介面的方法都對應著一個服務條目,服務條目的Id為: 方法的完全限定名 + 引數名
-
您可以在應用介面層對方法的快取、路由、服務治理、分布式事務進行相關配置,該部分內容請參考官方檔案
-
網關或是其他模塊的微服務專案需要參考服務應用介面專案或是通過nuget的方式安裝服務應用介面生成的包,
-
[Governance(ProhibitExtranet = true)]可以標識一個方法禁止與集群外部進行通信,通過網關也不會生成swagger檔案, -
應用介面方法生成的WebApi支持restful API風格,Lms支持通過方法的約定命名生成對應http方法請求的WebApi,您當然開發者也可以通過
HttpMethodAttribute特性對某個方法進行注解,
一個典型的應用介面的定義
/// <summary>
/// 賬號服務
/// </summary>
[ServiceRoute]
public interface IAccountAppService
{
/// <summary>
/// 新增賬號
/// </summary>
/// <param name="input">賬號資訊</param>
/// <returns></returns>
Task<GetAccountOutput> Create(CreateAccountInput input);
/// <summary>
/// 通過賬號名稱獲取賬號
/// </summary>
/// <param name="name">賬號名稱</param>
/// <returns></returns>
[GetCachingIntercept("Account:Name:{0}")]
[HttpGet("{name:string}")]
Task<GetAccountOutput> GetAccountByName([CacheKey(0)] string name);
/// <summary>
/// 通過Id獲取賬號資訊
/// </summary>
/// <param name="id">賬號Id</param>
/// <returns></returns>
[GetCachingIntercept("Account:Id:{0}")]
[HttpGet("{id:long}")]
Task<GetAccountOutput> GetAccountById([CacheKey(0)] long id);
/// <summary>
/// 更新賬號資訊
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[UpdateCachingIntercept( "Account:Id:{0}")]
Task<GetAccountOutput> Update(UpdateAccountInput input);
/// <summary>
/// 洗掉賬號資訊
/// </summary>
/// <param name="id">賬號Id</param>
/// <returns></returns>
[RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
[HttpDelete("{id:long}")]
Task Delete([CacheKey(0)]long id);
/// <summary>
/// 訂單扣款
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Governance(ProhibitExtranet = true)]
[RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
[Transaction]
Task<long?> DeductBalance(DeductBalanceInput input);
}
應用服務--應用介面的實作
-
應用服務層只需要參考應用服務介面層以及領域服務層,并實作應用介面相關的方法,
-
確保該微服務模塊的主機參考了該模塊的應用服務層,這樣主機才能夠托管該應用本身,
-
應用服務層可以通過參考其他微服務模塊的應用介面層專案(或是安裝nuget包,取決于開發團隊的專案管理方法),與其他微服務模塊進行rpc通信,
-
應用服務層需要依賴領域服務,通過呼叫領域服務的相關介面,實作該模塊的核心業務邏輯,
-
DTO到物體物件或是物體對DTO物件的映射關系可以在該層指定映射關系,
一個典型的應用服務的實作如下所示:
public class AccountAppService : IAccountAppService
{
private readonly IAccountDomainService _accountDomainService;
public AccountAppService(IAccountDomainService accountDomainService)
{
_accountDomainService = accountDomainService;
}
public async Task<GetAccountOutput> Create(CreateAccountInput input)
{
var account = input.MapTo<Domain.Accounts.Account>();
account = await _accountDomainService.Create(account);
return account.MapTo<GetAccountOutput>();
}
public async Task<GetAccountOutput> GetAccountByName(string name)
{
var account = await _accountDomainService.GetAccountByName(name);
return account.MapTo<GetAccountOutput>();
}
public async Task<GetAccountOutput> GetAccountById(long id)
{
var account = await _accountDomainService.GetAccountById(id);
return account.MapTo<GetAccountOutput>();
}
public async Task<GetAccountOutput> Update(UpdateAccountInput input)
{
var account = await _accountDomainService.Update(input);
return account.MapTo<GetAccountOutput>();
}
public Task Delete(long id)
{
return _accountDomainService.Delete(id);
}
[TccTransaction(ConfirmMethod = "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")]
public async Task<long?> DeductBalance(DeductBalanceInput input)
{
var account = await _accountDomainService.GetAccountById(input.AccountId);
if (input.OrderBalance > account.Balance)
{
throw new BusinessException("賬號余額不足");
}
return await _accountDomainService.DeductBalance(input, TccMethodType.Try);
}
public Task DeductBalanceConfirm(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Confirm);
}
public Task DeductBalanceCancel(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Cancel);
}
}
領域層--微服務的核心業務實作
-
領域層是該微服務模塊核心業務處理的模塊,一般用于定于聚合根、物體、領域服務、倉儲等業務物件,
-
領域層參考該微服務模塊的應用介面層,方便使用dto物件,
-
領域層可以通過參考其他微服務模塊的應用介面層專案(或是安裝nuget包,取決于開發團隊的專案管理方法),與其他微服務模塊進行rpc通信,
-
領域服務必須要直接或間接繼承
ITransientDependency介面,這樣,該領域服務才會被注入到ioc容器, -
lms.samples 專案使用TanvirArjel.EFCore.GenericRepository包實作資料的讀寫操作,
一個典型的領域服務的實作如下所示:
public class AccountDomainService : IAccountDomainService
{
private readonly IRepository _repository;
private readonly IDistributedCache<GetAccountOutput, string> _accountCache;
public AccountDomainService(IRepository repository,
IDistributedCache<GetAccountOutput, string> accountCache)
{
_repository = repository;
_accountCache = accountCache;
}
public async Task<Account> Create(Account account)
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Name == account.Name);
if (exsitAccountCount > 0)
{
throw new BusinessException($"已經存在{account.Name}名稱的賬號");
}
exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Email == account.Email);
if (exsitAccountCount > 0)
{
throw new BusinessException($"已經存在{account.Email}Email的賬號");
}
await _repository.InsertAsync<Account>(account);
return account;
}
public async Task<Account> GetAccountByName(string name)
{
var accountEntry = _repository.GetQueryable<Account>().FirstOrDefault(p => p.Name == name);
if (accountEntry == null)
{
throw new BusinessException($"不存在名稱為{name}的賬號");
}
return accountEntry;
}
public async Task<Account> GetAccountById(long id)
{
var accountEntry = _repository.GetQueryable<Account>().FirstOrDefault(p => p.Id == id);
if (accountEntry == null)
{
throw new BusinessException($"不存在Id為{id}的賬號");
}
return accountEntry;
}
public async Task<Account> Update(UpdateAccountInput input)
{
var account = await GetAccountById(input.Id);
if (!account.Email.Equals(input.Email))
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Email == input.Email);
if (exsitAccountCount > 0)
{
throw new BusinessException($"系統中已經存在Email為{input.Email}的賬號");
}
}
if (!account.Name.Equals(input.Name))
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Name == input.Name);
if (exsitAccountCount > 0)
{
throw new BusinessException($"系統中已經存在Name為{input.Name}的賬號");
}
}
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
account = input.MapTo(account);
await _repository.UpdateAsync(account);
return account;
}
public async Task Delete(long id)
{
var account = await GetAccountById(id);
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
await _repository.DeleteAsync(account);
}
public async Task<long?> DeductBalance(DeductBalanceInput input, TccMethodType tccMethodType)
{
var account = await GetAccountById(input.AccountId);
var trans = await _repository.BeginTransactionAsync();
BalanceRecord balanceRecord = null;
switch (tccMethodType)
{
case TccMethodType.Try:
account.Balance -= input.OrderBalance;
account.LockBalance += input.OrderBalance;
balanceRecord = new BalanceRecord()
{
OrderBalance = input.OrderBalance,
OrderId = input.OrderId,
PayStatus = PayStatus.NoPay
};
await _repository.InsertAsync(balanceRecord);
RpcContext.GetContext().SetAttachment("balanceRecordId",balanceRecord.Id);
break;
case TccMethodType.Confirm:
account.LockBalance -= input.OrderBalance;
var balanceRecordId1 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId1.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId1.Value);
balanceRecord.PayStatus = PayStatus.Payed;
await _repository.UpdateAsync(balanceRecord);
}
break;
case TccMethodType.Cancel:
account.Balance += input.OrderBalance;
account.LockBalance -= input.OrderBalance;
var balanceRecordId2 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId2.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId2.Value);
balanceRecord.PayStatus = PayStatus.Cancel;
await _repository.UpdateAsync(balanceRecord);
}
break;
}
await _repository.UpdateAsync(account);
await trans.CommitAsync();
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
return balanceRecord?.Id;
}
}
資料訪問(EntityFrameworkCore)--通過efcore實作資料讀寫
-
lms.samples專案使用orm框架efcore進行資料讀寫,
-
lms提供了
IConfigureService,通過繼承該介面即可使用IServiceCollection的實體指定資料背景關系物件和注冊倉庫服務,
public class EfCoreConfigureService : IConfigureService
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<OrderDbContext>(opt =>
opt.UseMySql(configuration.GetConnectionString("Default"),
ServerVersion.AutoDetect(configuration.GetConnectionString("Default"))))
.AddGenericRepository<OrderDbContext>(ServiceLifetime.Transient)
;
}
public int Order { get; } = 1;
}
-
主機專案需要顯式的參考該專案,只有這樣,該專案的
ConfigureServices才會被呼叫, -
資料遷移,請參考
應用啟動與除錯
獲取原始碼
- 使用git 克隆lms專案源代碼,lms.samples存放在
samples目錄下
# github
git clone https://github.com/liuhll/lms.git
# gitee
git clone https://gitee.com/liuhll2/lms.git
必要的前提
-
服務注冊中心
zookeeper -
快取服務
redis -
mysql資料庫
如果您電腦已經安裝了docker以及docker-compose命令,那么您只需要進入samples\docker-compose\infrastr目錄下,打開命令列作業,執行如下命令就可以自動安裝zookeeper、redis、mysql等服務:
docker-compose -f .\docker-compose.mysql.yml -f .\docker-compose.redis.yml -f .\docker-compose.zookeeper.yml up -d
資料庫遷移
需要分別進入到各個微服務模塊下的EntityFrameworkCore專案(例如:),執行如下命令:
dotnet ef database update
例如: 需要遷移account模塊的資料庫如下所示:

order模塊和stock模塊與account模塊一致,在服務運行前都需要通過資料庫遷移命令生成相關資料庫,
資料庫遷移指定資料庫連接地址默認指定的是
appsettings.Development.yml中配置的,您可以通過修改該組態檔中的connectionStrings.default配置項來指定自己的資料庫服務地址,如果沒有
dotnet ef命令,則需要通過dotnet tool install --global dotnet-ef安裝ef工具,請[參考] (https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/install)
以專案的方式啟動和除錯
使用visual studio作為開發工具
進入到samples目錄下,使用visual studio打開lms.samples.sln解決方案,將專案設定為多啟動專案,并將網關和各個模塊的微服務主機設定為啟動專案,如下圖:
(
)
設定完成后直接啟動即可,
使用rider作為開發工具
- 進入到samples目錄下,使用rider打開
lms.samples.sln解決方案,打開各個微服務模塊下的Properties/launchSettings.json,點擊圖中綠色的箭頭即可啟動專案,
(
)
- 啟動網關專案后,可以看到應用介面的服務條目生成的swagger api檔案 http://localhost:5000/swagger,
(
)
默認的環境變數為:
Development,如果需要修改環境變數的話,可以通過Properties/launchSettings.json下的environmentVariables節點修改相關環境變數,請參考在 ASP.NET Core 中使用多個環境,資料庫連接、服務注冊中心地址、以及redis快取地址和分布式鎖連接等配置項可以通過修改
appsettings.Development.yml配置項自定義指定,
以docker-compose的方式啟動和除錯
-
進入到samples目錄下,使用visual studio打開
lms.samples.dockercompose.sln解決方案,將docker-compose設定為啟動專案,即可啟動和調式, -
應用啟動成功后,打開: http://127.0.0.1/swagger,即可看到swagger api檔案
(
)
以docker-compose的方式啟動和除錯,則指定的環境變數為:
ContainerDev資料庫連接、服務注冊中心地址、以及redis快取地址和分布式鎖連接等配置項可以通過修改
appsettings.ContainerDev.yml配置項自定義指定,配置的服務連接地址不允許為:127.0.0.1或是localhost
測驗和調式
服務啟動成功后,您可以通過寫入/api/account-post介面和/api/product-post新增賬號和產品,然后通過/api/order-post介面進行測驗和調式,
開源地址
github: https://github.com/liuhll/lms
gitee: https://gitee.com/liuhll2/lms
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/273954.html
標籤:.NET技术
