經過lms.samples熟悉lms微服務框架的使用

通過一段時間的開發與測試,終於發佈了Lms框架的第一個正式版本(1.0.0版本),並給出了lms框架的樣例項目lms.samples。本文經過對lms.samples的介紹,簡述如何經過lms框架快速的構建一個微服務的業務框架,並進行應用開發。mysql

lms.samples項目基本介紹

lms.sample項目由三個獨立的微服務應用模塊組成:account、stock、order和一個網關項目gateway構成。git

業務應用模塊

每一個獨立的微服務應用採用模塊化設計,主要由以下幾部分組成:github

  1. 主機(Host): 主要用於託管微服務應用自己,主機經過引用應用服務項目(應用接口的實現),託管微服務應用,經過託管應用服務,在主機啓動的過程當中,向服務註冊中心註冊服務路由。redis

  2. 應用接口層(Application.Contracts): 用於定義應用服務接口,經過應用接口,該微服務模塊與其餘微服務模塊或是網關進行rpc通訊的能力。在該項目中,除了定義應用服務接口以前,通常還定義與該應用接口相關的DTO對象。應用接口除了被該微服務應用項目引用,並實現應用服務以前,還能夠被網關或是其餘微服務模塊引用。網關或是其餘微服務項目經過應用接口生成的代理與該微服務模塊經過rpc進行通訊。sql

  3. 應用服務層(Application): 應用服務是該微服務定義的應用接口的實現。應用服務與DDD傳統分層架構的應用層的概念一致。主要負責外部通訊與領域層之間的協調。通常地,應用服務進行業務流程控制,可是不包含業務邏輯的實現。docker

  4. 領域層(Domain): 負責表達業務概念,業務狀態信息以及業務規則,是該微服務模塊的業務核心。通常地,在該層能夠定義聚合根、實體、領域服務等對象。數據庫

  5. 領域共享層(Domain.Shared): 該層用於定義與領域對象相關的模型、實體等相關類型。不包含任何業務實現,能夠被其餘微服務引用。json

  6. 數據訪問(DataAccess)層: 該層通常用於封裝數據訪問相關的對象。例如:倉庫對象、 SqlHelper、或是ORM相關的類型等。在lms.samples中,經過efcore實現數據的讀寫操做。windows

(image)api

服務聚合與網關

lms框架不容許服務外部與微服務主機直接通訊,應用請求必須經過http請求到達網關,網關經過lms提供的中間件解析到服務條目,並經過rpc與集羣內部的微服務進行通訊。因此,若是服務須要與集羣外部進行通訊,那麼,開發者定義的網關必需要引用各個微服務模塊的應用接口層;以及必需要使用lms相關的中間件。

開發環境

  1. .net版本: 5.0.101

  2. lms版本: 1.0.0

  3. IDE: (1) visual studio 最新版 (2) Rider(推薦)

主機與應用託管

主機的建立步驟

經過lms框架建立一個業務模塊很是方便,只須要經過以下4個步驟,就能夠輕鬆的建立一個lms應用業務模塊。

  1. 建立項目

建立控制檯應用(Console Application)項目,而且引用Silky.Lms.NormHost包。

dotnet add package Silky.Lms.NormHost --version 1.0.0
  1. 應用程序入口與主機構建

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>()
                ;
        }
    }
  1. 配置文件

lms框架支持yml或是json格式做爲配置文件。經過appsettings.yml對lms框架進行統一配置,經過appsettings.${Environment}.yml對不一樣環境變量下的配置項進行設置。

開發者若是直接經過項目的方式啓動應用,那麼能夠經過Properties/launchSettings.jsonenvironmentVariables.DOTNET_ENVIRONMENT環境變量。若是經過docker-compose的方式啓動應用,那麼能夠經過.env設置DOTNET_ENVIRONMENT環境變量。

爲保證配置文件有效,開發者須要顯式的將配置文件拷貝到項目生成目錄下。

  1. 引用應用服務層和數據訪問層

通常地,主機項目須要引用該微服務模塊的應用服務層和數據訪問層。只有主機引用應用服務層,主機在啓動時,纔會生成服務條目的路由,而且將服務路由註冊到服務註冊中心。

一個典型的主機項目文件以下所示:

<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包。

  1. 開發者只須要在應用接口經過ServiceRouteAttribute特性對應用接口進行直接便可。

  2. Lms約定應用接口應當以IXxxAppService命名,這樣,服務條目生成的路由則會以api/xxx形式生成。固然這並非強制的。

  3. 每一個應用接口的方法都對應着一個服務條目,服務條目的Id爲: 方法的徹底限定名 + 參數名

  4. 您能夠在應用接口層對方法的緩存、路由、服務治理、分佈式事務進行相關配置。該部份內容請參考官方文檔

  5. 網關或是其餘模塊的微服務項目須要引用服務應用接口項目或是經過nuget的方式安裝服務應用接口生成的包。

  6. [Governance(ProhibitExtranet = true)]能夠標識一個方法禁止與集羣外部進行通訊,經過網關也不會生成swagger文檔。

  7. 應用接口方法生成的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);
    }

應用服務--應用接口的實現

  1. 應用服務層只須要引用應用服務接口層以及領域服務層,並實現應用接口相關的方法。

  2. 確保該微服務模塊的主機引用了該模塊的應用服務層,這樣主機纔可以託管該應用自己。

  3. 應用服務層能夠經過引用其餘微服務模塊的應用接口層項目(或是安裝nuget包,取決於開發團隊的項目管理方法),與其餘微服務模塊進行rpc通訊。

  4. 應用服務層須要依賴領域服務,經過調用領域服務的相關接口,實現該模塊的核心業務邏輯。

  5. 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);
        }
    }

領域層--微服務的核心業務實現

  1. 領域層是該微服務模塊核心業務處理的模塊,通常用於定於聚合根、實體、領域服務、倉儲等業務對象。

  2. 領域層引用該微服務模塊的應用接口層,方便使用dto對象。

  3. 領域層能夠經過引用其餘微服務模塊的應用接口層項目(或是安裝nuget包,取決於開發團隊的項目管理方法),與其餘微服務模塊進行rpc通訊。

  4. 領域服務必需要直接或間接繼承ITransientDependency接口,這樣,該領域服務纔會被注入到ioc容器。

  5. 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實現數據讀寫

  1. lms.samples項目使用orm框架efcore進行數據讀寫。

  2. 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;
    }
  1. 主機項目須要顯式的引用該項目,只有這樣,該項目的ConfigureServices纔會被調用。

  2. 數據遷移,請參考

應用啓動與調試

獲取源碼

  1. 使用git 克隆lms項目源代碼,lms.samples存放在samples目錄下
# github
git clone https://github.com/liuhll/lms.git

# gitee
git clone https://gitee.com/liuhll2/lms.git

必要的前提

  1. 服務註冊中心zookeeper

  2. 緩存服務redis

  3. mysql數據庫

若是您電腦已經安裝了docker以及docker-compose命令,那麼您只須要進入samples\docker-compose\infrastr目錄下,打開命令行工做,執行以下命令就能夠自動安裝zookeeperredismysql等服務:

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模塊的數據庫以下所示:

image

order模塊和stock模塊與account模塊一致,在服務運行前都須要經過數據庫遷移命令生成相關數據庫。

  1. 數據庫遷移指定數據庫鏈接地址默認指定的是appsettings.Development.yml中配置的,您能夠經過修改該配置文件中的connectionStrings.default配置項來指定本身的數據庫服務地址。

  2. 若是沒有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解決方案,將項目設置爲多啓動項目,並將網關和各個模塊的微服務主機設置爲啓動項目,以下圖:

(image)

設置完成後直接啓動便可。

使用rider做爲開發工具

  1. 進入到samples目錄下,使用rider打開lms.samples.sln解決方案,打開各個微服務模塊下的Properties/launchSettings.json,點擊圖中綠色的箭頭便可啓動項目。

(image)

  1. 啓動網關項目後,能夠看到應用接口的服務條目生成的swagger api文檔 http://localhost:5000/swagger

(image)

  1. 默認的環境變量爲: Development,若是須要修改環境變量的話,能夠經過Properties/launchSettings.json下的environmentVariables節點修改相關環境變量,請參考在 ASP.NET Core 中使用多個環境

  2. 數據庫鏈接、服務註冊中心地址、以及redis緩存地址和分佈式鎖鏈接等配置項能夠經過修改appsettings.Development.yml配置項自定義指定。

以docker-compose的方式啓動和調試

  1. 進入到samples目錄下,使用visual studio打開lms.samples.dockercompose.sln解決方案,將docker-compose設置爲啓動項目,便可啓動和調式。

  2. 應用啓動成功後,打開: http://127.0.0.1/swagger,便可看到swagger api文檔

(image)

  1. 以docker-compose的方式啓動和調試,則指定的環境變量爲:ContainerDev

  2. 數據庫鏈接、服務註冊中心地址、以及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

相關文章
相關標籤/搜索