diff --git b/.gitattributes a/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ a/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git b/.gitignore a/.gitignore new file mode 100644 index 0000000..64f27d9 --- /dev/null +++ a/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + diff --git b/Pole.sln a/Pole.sln new file mode 100644 index 0000000..4adbd7f --- /dev/null +++ a/Pole.sln @@ -0,0 +1,72 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29519.87 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9932C965-8B38-4F70-9E43-86DC56860E2B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{655E719B-4A3E-467C-A541-E0770AB81DE1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pole.Core", "src\Pole.Core\Pole.Core.csproj", "{CA80F6EF-95A0-4BB7-BA8B-02E167E82865}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Application", "src\Pole.Application\Pole.Application.csproj", "{C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Domain", "src\Pole.Domain\Pole.Domain.csproj", "{6F6DBA49-4274-4C62-BBE7-91FCC5B77989}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Domain.EntityframeworkCore", "src\Pole.Domain.EntityframeworkCore\Pole.Domain.EntityframeworkCore.csproj", "{1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Grpc", "src\Pole.Grpc\Pole.Grpc.csproj", "{F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{4A0FB696-EC29-4A5F-B40B-A6FC56001ADB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apis", "apis", "{475116FC-DEEC-4255-94E4-AE7B8C85038D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Product.Api", "samples\apis\Product.Api\Product.Api.csproj", "{6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA80F6EF-95A0-4BB7-BA8B-02E167E82865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA80F6EF-95A0-4BB7-BA8B-02E167E82865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA80F6EF-95A0-4BB7-BA8B-02E167E82865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA80F6EF-95A0-4BB7-BA8B-02E167E82865}.Release|Any CPU.Build.0 = Release|Any CPU + {C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C}.Release|Any CPU.Build.0 = Release|Any CPU + {6F6DBA49-4274-4C62-BBE7-91FCC5B77989}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F6DBA49-4274-4C62-BBE7-91FCC5B77989}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F6DBA49-4274-4C62-BBE7-91FCC5B77989}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F6DBA49-4274-4C62-BBE7-91FCC5B77989}.Release|Any CPU.Build.0 = Release|Any CPU + {1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A}.Release|Any CPU.Build.0 = Release|Any CPU + {F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8}.Release|Any CPU.Build.0 = Release|Any CPU + {6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CA80F6EF-95A0-4BB7-BA8B-02E167E82865} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {C7825E5B-4FB0-4498-B8D1-E9EC0BC1AA5C} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {6F6DBA49-4274-4C62-BBE7-91FCC5B77989} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {1C26BE3A-CBEA-47D1-97A0-6DB4F41DFF5A} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {F40FE25F-6081-4B29-A7BD-CB5C24F6FDA8} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {475116FC-DEEC-4255-94E4-AE7B8C85038D} = {4A0FB696-EC29-4A5F-B40B-A6FC56001ADB} + {6A68E63D-ED4B-4F46-9A2E-AA7FE2B0032E} = {475116FC-DEEC-4255-94E4-AE7B8C85038D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DB0775A3-F293-4043-ADB7-72BAC081E87E} + EndGlobalSection +EndGlobal diff --git b/samples/apis/Product.Api/Product.Api.csproj a/samples/apis/Product.Api/Product.Api.csproj new file mode 100644 index 0000000..3d293a8 --- /dev/null +++ a/samples/apis/Product.Api/Product.Api.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.1 + + + + + + + + + + + diff --git b/samples/apis/Product.Api/Program.cs a/samples/apis/Product.Api/Program.cs new file mode 100644 index 0000000..24b5f80 --- /dev/null +++ a/samples/apis/Product.Api/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Product.Api +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + // Additional configuration is required to successfully run gRPC on macOS. + // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git b/samples/apis/Product.Api/Properties/launchSettings.json a/samples/apis/Product.Api/Properties/launchSettings.json new file mode 100644 index 0000000..0e1df79 --- /dev/null +++ a/samples/apis/Product.Api/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Product.Api": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git b/samples/apis/Product.Api/Protos/greet.proto a/samples/apis/Product.Api/Protos/greet.proto new file mode 100644 index 0000000..26b73fb --- /dev/null +++ a/samples/apis/Product.Api/Protos/greet.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +option csharp_namespace = "Product.Api"; + +package greet; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings. +message HelloReply { + string message = 1; +} diff --git b/samples/apis/Product.Api/Services/GreeterService.cs a/samples/apis/Product.Api/Services/GreeterService.cs new file mode 100644 index 0000000..9c4361f --- /dev/null +++ a/samples/apis/Product.Api/Services/GreeterService.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Extensions.Logging; + +namespace Product.Api +{ + public class GreeterService : Greeter.GreeterBase + { + private readonly ILogger _logger; + public GreeterService(ILogger logger) + { + _logger = logger; + } + + public override Task SayHello(HelloRequest request, ServerCallContext context) + { + return Task.FromResult(new HelloReply + { + Message = "Hello " + request.Name + }); + } + } +} diff --git b/samples/apis/Product.Api/Startup.cs a/samples/apis/Product.Api/Startup.cs new file mode 100644 index 0000000..35d902b --- /dev/null +++ a/samples/apis/Product.Api/Startup.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Product.Api +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + }); + }); + } + } +} diff --git b/samples/apis/Product.Api/appsettings.Development.json a/samples/apis/Product.Api/appsettings.Development.json new file mode 100644 index 0000000..fe20c40 --- /dev/null +++ a/samples/apis/Product.Api/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Grpc": "Information", + "Microsoft": "Information" + } + } +} diff --git b/samples/apis/Product.Api/appsettings.json a/samples/apis/Product.Api/appsettings.json new file mode 100644 index 0000000..1f29241 --- /dev/null +++ a/samples/apis/Product.Api/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git b/src/Pole.Application/Cqrs/CommandResult.cs a/src/Pole.Application/Cqrs/CommandResult.cs new file mode 100644 index 0000000..50f4635 --- /dev/null +++ a/src/Pole.Application/Cqrs/CommandResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Application.Cqrs +{ + public class CommandResult + { + public CommandResult(int status,string message) + { + Status = status; + Message = message; + } + public static CommandResult SuccessResult = new CommandResult(1, "操作成功"); + /// + /// 1 Command Success 2 Command Faild ... + /// + public int Status { get;private set; } + public string Message { get;private set; } + } +} diff --git b/src/Pole.Application/Cqrs/ICommandBus.cs a/src/Pole.Application/Cqrs/ICommandBus.cs new file mode 100644 index 0000000..4d531d9 --- /dev/null +++ a/src/Pole.Application/Cqrs/ICommandBus.cs @@ -0,0 +1,15 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pole.Application.Cqrs +{ + public interface ICommandBus + { + Task Send(IRequest request, CancellationToken cancellationToken = default); + Task Send(object request, CancellationToken cancellationToken = default); + } +} diff --git b/src/Pole.Application/Cqrs/ICommandHandler.cs a/src/Pole.Application/Cqrs/ICommandHandler.cs new file mode 100644 index 0000000..a67af1d --- /dev/null +++ a/src/Pole.Application/Cqrs/ICommandHandler.cs @@ -0,0 +1,11 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Application.Cqrs +{ + public interface ICommandHandler: IRequestHandler where TCommand : IRequest + { + } +} diff --git b/src/Pole.Application/Cqrs/IQueries.cs a/src/Pole.Application/Cqrs/IQueries.cs new file mode 100644 index 0000000..1cdf5c6 --- /dev/null +++ a/src/Pole.Application/Cqrs/IQueries.cs @@ -0,0 +1,12 @@ +using Pole.Core.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Application.Cqrs +{ + public interface IQueries: IScopedDenpendency + { + + } +} diff --git b/src/Pole.Application/Cqrs/Internal/DefaultCommandBus.cs a/src/Pole.Application/Cqrs/Internal/DefaultCommandBus.cs new file mode 100644 index 0000000..74acfeb --- /dev/null +++ a/src/Pole.Application/Cqrs/Internal/DefaultCommandBus.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediatR; + +namespace Pole.Application.Cqrs.Internal +{ + class DefaultCommandBus : ICommandBus + { + private readonly IMediator _mediator; + public DefaultCommandBus(IMediator mediator) + { + _mediator = mediator; + } + public Task Send(IRequest request, CancellationToken cancellationToken = default) + { + return _mediator.Send(request, cancellationToken); + } + + public Task Send(object request, CancellationToken cancellationToken = default) + { + return _mediator.Send(request, cancellationToken); + } + } +} diff --git b/src/Pole.Application/MediatR/MediatRServiceConfigurationExtensions.cs a/src/Pole.Application/MediatR/MediatRServiceConfigurationExtensions.cs new file mode 100644 index 0000000..af5a726 --- /dev/null +++ a/src/Pole.Application/MediatR/MediatRServiceConfigurationExtensions.cs @@ -0,0 +1,28 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Application.MediatR +{ + public static class MediatRServiceConfigurationExtensions + { + public static MediatRServiceConfiguration AddServiceLifetime(this MediatRServiceConfiguration configuration, ServiceLifetime lifetime) + { + if (lifetime == ServiceLifetime.Scoped) + { + configuration.AsScoped(); + } + else if (lifetime == ServiceLifetime.Singleton) + { + configuration.AsSingleton(); + } + else + { + configuration.AsTransient(); + } + return configuration; + } + } +} diff --git b/src/Pole.Application/Options.cs a/src/Pole.Application/Options.cs new file mode 100644 index 0000000..c36a780 --- /dev/null +++ a/src/Pole.Application/Options.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Pole.Application +{ + public class Options + { + public IServiceCollection Services { get; set; } + public IEnumerable ApplicationAssemblies { get; set; } + + } +} diff --git b/src/Pole.Application/OptionsExtensions.cs a/src/Pole.Application/OptionsExtensions.cs new file mode 100644 index 0000000..7e9c02b --- /dev/null +++ a/src/Pole.Application/OptionsExtensions.cs @@ -0,0 +1,44 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Pole.Application.Cqrs; +using Pole.Application.MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Pole.Application +{ + public static class OptionsExtensions + { + public static Options AutoInjectionQueries(this Options options) + { + var assemblies = options.ApplicationAssemblies; + + foreach (var assembly in assemblies) + { + var queriesImplements = assembly.GetTypes().Where(m => typeof(IQueries).IsAssignableFrom(m) && m.IsClass && !m.IsAbstract); + foreach (var queriesImplement in queriesImplements) + { + var queriesService = queriesImplement.GetInterfaces().FirstOrDefault(); + options.Services.AddScoped(queriesService, queriesImplement); + } + } + return options; + } + public static Options AddApplicationAssemblies(this Options options, params Assembly[] assemblies) + { + options.ApplicationAssemblies = assemblies.AsEnumerable(); + return options; + } + public static Options AutoInjectionCommandHandlersAndDomainEventHandlers(this Options options, ServiceLifetime lifetime = ServiceLifetime.Scoped) + { + options.Services.AddMediatR(config => + { + config.AddServiceLifetime(lifetime); + }, options.ApplicationAssemblies.ToArray()); + return options; + } + } +} diff --git b/src/Pole.Application/Pole.Application.csproj a/src/Pole.Application/Pole.Application.csproj new file mode 100644 index 0000000..4021574 --- /dev/null +++ a/src/Pole.Application/Pole.Application.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git b/src/Pole.Application/ServiceCollectionExtensions.cs a/src/Pole.Application/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..71e1c37 --- /dev/null +++ a/src/Pole.Application/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; +using MediatR; +using System.Reflection; +using Pole.Application.Cqrs; +using Pole.Application.Cqrs.Internal; + +namespace Pole.Application +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddPoleApplication(this IServiceCollection services, Options options) + { + services.AddScoped(); + + return services; + } + } +} diff --git b/src/Pole.Core/DependencyInjection/IScopedDenpendency.cs a/src/Pole.Core/DependencyInjection/IScopedDenpendency.cs new file mode 100644 index 0000000..abe7aae --- /dev/null +++ a/src/Pole.Core/DependencyInjection/IScopedDenpendency.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Core.DependencyInjection +{ + public interface IScopedDenpendency + { + } +} diff --git b/src/Pole.Core/DependencyInjection/ISingleDependency.cs a/src/Pole.Core/DependencyInjection/ISingleDependency.cs new file mode 100644 index 0000000..33e6a12 --- /dev/null +++ a/src/Pole.Core/DependencyInjection/ISingleDependency.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Core.DependencyInjection +{ + public interface ISingleDependency + { + } +} diff --git b/src/Pole.Core/DependencyInjection/ITransientDependency.cs a/src/Pole.Core/DependencyInjection/ITransientDependency.cs new file mode 100644 index 0000000..ef11b57 --- /dev/null +++ a/src/Pole.Core/DependencyInjection/ITransientDependency.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Core.DependencyInjection +{ + public interface ITransientDependency + { + } +} diff --git b/src/Pole.Core/Pole.Core.csproj a/src/Pole.Core/Pole.Core.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ a/src/Pole.Core/Pole.Core.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git b/src/Pole.Domain.EntityframeworkCore/DbContextBase.cs a/src/Pole.Domain.EntityframeworkCore/DbContextBase.cs new file mode 100644 index 0000000..06a341e --- /dev/null +++ a/src/Pole.Domain.EntityframeworkCore/DbContextBase.cs @@ -0,0 +1,36 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Pole.Domain; +using Pole.Domain.UnitOfWork; +using Pole.EntityframeworkCore.MediatR; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pole.EntityframeworkCore +{ + public class DbContextBase : DbContext, IUnitOfWork + { + private readonly IMediator _mediator; + public DbContextBase(DbContextOptions options, IMediator mediator) : base(options) + { + _mediator = mediator; + } + + public async Task CompeleteAsync(CancellationToken cancellationToken = default) + { + var result = CompleteResult.SuccessResult; + + var eventHnadlersResult = await _mediator.DispatchDomainEventsAsync(this); + if (eventHnadlersResult.Status != 1) + { + return new CompleteResult(eventHnadlersResult); + } + var dbResult = await base.SaveChangesAsync(cancellationToken); + + return result; + } + } +} diff --git b/src/Pole.Domain.EntityframeworkCore/MediatR/MediatorExtension.cs a/src/Pole.Domain.EntityframeworkCore/MediatR/MediatorExtension.cs new file mode 100644 index 0000000..8c68e99 --- /dev/null +++ a/src/Pole.Domain.EntityframeworkCore/MediatR/MediatorExtension.cs @@ -0,0 +1,41 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Pole.Domain; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.EntityframeworkCore.MediatR +{ + public static class MediatorExtension + { + public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx) + { + var result = DomainHandleResult.SuccessResult; + + var domainEntities = ctx.ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + domainEntities.ToList() + .ForEach(entity => entity.Entity.ClearDomainEvents()); + + foreach(var domainEvent in domainEvents) + { + var currentDomainHandleResult = await mediator.Send(domainEvent); + if (currentDomainHandleResult.Status != 1) + { + result = currentDomainHandleResult; + break; + } + } + return result; + } + } +} diff --git b/src/Pole.Domain.EntityframeworkCore/Pole.Domain.EntityframeworkCore.csproj a/src/Pole.Domain.EntityframeworkCore/Pole.Domain.EntityframeworkCore.csproj new file mode 100644 index 0000000..bc458df --- /dev/null +++ a/src/Pole.Domain.EntityframeworkCore/Pole.Domain.EntityframeworkCore.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git b/src/Pole.Domain/DomainHandleResult.cs a/src/Pole.Domain/DomainHandleResult.cs new file mode 100644 index 0000000..457f127 --- /dev/null +++ a/src/Pole.Domain/DomainHandleResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain +{ + public class DomainHandleResult + { + public DomainHandleResult(int status, string message) + { + Status = status; + Message = message; + } + public static DomainHandleResult SuccessResult = new DomainHandleResult(1, "处理成功"); + /// + /// 1 Success 2 Faild ... + /// + public int Status { get;private set; } + public string Message { get;private set; } + } +} diff --git b/src/Pole.Domain/Entity/AggregateRoot.cs a/src/Pole.Domain/Entity/AggregateRoot.cs new file mode 100644 index 0000000..8a87e32 --- /dev/null +++ a/src/Pole.Domain/Entity/AggregateRoot.cs @@ -0,0 +1,9 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain +{ + public interface AggregateRoot { } +} diff --git b/src/Pole.Domain/Entity/Entity.cs a/src/Pole.Domain/Entity/Entity.cs new file mode 100644 index 0000000..82d67ba --- /dev/null +++ a/src/Pole.Domain/Entity/Entity.cs @@ -0,0 +1,75 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain +{ + public abstract class Entity + { + string _id; + public virtual string Id + { + get + { + return _id; + } + protected set + { + _id = value; + } + } + public List DomainEvents { get; private set; } + public bool IsTransient() + { + return string.IsNullOrEmpty( this._id); + } + public override bool Equals(object obj) + { + if (obj == null || !(obj is Entity)) + return false; + + if (Object.ReferenceEquals(this, obj)) + return true; + + if (this.GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || this.IsTransient()) + return false; + else + return item.Id == this.Id; + } + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + public static bool operator ==(Entity left, Entity right) + { + if (Object.Equals(left, null)) + return (Object.Equals(right, null)) ? true : false; + else + return left.Equals(right); + } + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } + public void AddDomainEvent(IDomainEvent eventItem) + { + DomainEvents = DomainEvents ?? new List(); + DomainEvents.Add(eventItem); + } + public void RemoveDomainEvent(IDomainEvent eventItem) + { + if (DomainEvents is null) return; + DomainEvents.Remove(eventItem); + } + public void ClearDomainEvents() + { + DomainEvents?.Clear(); + } + } +} diff --git b/src/Pole.Domain/Entity/Enumeration.cs a/src/Pole.Domain/Entity/Enumeration.cs new file mode 100644 index 0000000..f3cfcb6 --- /dev/null +++ a/src/Pole.Domain/Entity/Enumeration.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Pole.Domain +{ + public abstract class Enumeration : IComparable + { + public string Name { get; private set; } + + public int Id { get; private set; } + + protected Enumeration() + { + } + + protected Enumeration(int id, string name) + { + Id = id; + Name = name; + } + + public override string ToString() + { + return Name; + } + + public static IEnumerable GetAll() where T : Enumeration, new() + { + var type = typeof(T); + var fields = type.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + + foreach (var info in fields) + { + var instance = new T(); + + if (info.GetValue(instance) is T locatedValue) + { + yield return locatedValue; + } + } + } + + public override bool Equals(object obj) + { + var otherValue = obj as Enumeration; + + if (otherValue == null) + { + return false; + } + + var typeMatches = GetType().Equals(obj.GetType()); + var valueMatches = Id.Equals(otherValue.Id); + + return typeMatches && valueMatches; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) + { + var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id); + return absoluteDifference; + } + + public static T FromValue(int value) where T : Enumeration, new() + { + var matchingItem = Parse(value, "value", item => item.Id == value); + return matchingItem; + } + + public static T FromDisplayName(string displayName) where T : Enumeration, new() + { + var matchingItem = Parse(displayName, "display name", item => item.Name == displayName); + return matchingItem; + } + + private static T Parse(K value, string description, Func predicate) where T : Enumeration, new() + { + var matchingItem = GetAll().FirstOrDefault(predicate); + + if (matchingItem == null) + { + var message = string.Format("'{0}' is not a valid {1} in {2}", value, description, typeof(T)); + + throw new InvalidOperationException(message); + } + + return matchingItem; + } + + public int CompareTo(object other) + { + return Id.CompareTo(((Enumeration)other).Id); + } + } +} diff --git b/src/Pole.Domain/Entity/ISoftDeleteable.cs a/src/Pole.Domain/Entity/ISoftDeleteable.cs new file mode 100644 index 0000000..249f357 --- /dev/null +++ a/src/Pole.Domain/Entity/ISoftDeleteable.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain +{ + public interface ISoftDeleteable + { + bool IsDelete { get; set; } + } +} diff --git b/src/Pole.Domain/Entity/ValueObject.cs a/src/Pole.Domain/Entity/ValueObject.cs new file mode 100644 index 0000000..a338fe3 --- /dev/null +++ a/src/Pole.Domain/Entity/ValueObject.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Pole.Domain +{ + public abstract class ValueObject + { + protected static bool EqualOperator(ValueObject left, ValueObject right) + { + if (left is null ^ right is null) + { + return false; + } + return left is null || left.Equals(right); + } + + + protected static bool NotEqualOperator(ValueObject left, ValueObject right) + { + return !(EqualOperator(left, right)); + } + + + protected abstract IEnumerable GetAtomicValues(); + + + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + ValueObject other = (ValueObject)obj; + IEnumerator thisValues = GetAtomicValues().GetEnumerator(); + IEnumerator otherValues = other.GetAtomicValues().GetEnumerator(); + while (thisValues.MoveNext() && otherValues.MoveNext()) + { + if (thisValues.Current is null ^ otherValues.Current is null) + { + return false; + } + if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current)) + { + return false; + } + } + return !thisValues.MoveNext() && !otherValues.MoveNext(); + } + + + public override int GetHashCode() + { + return GetAtomicValues() + .Select(x => x != null ? x.GetHashCode() : 0) + .Aggregate((x, y) => x ^ y); + } + + public ValueObject GetCopy() + { + return this.MemberwiseClone() as ValueObject; + } + } + +} diff --git b/src/Pole.Domain/IDomainEvent.cs a/src/Pole.Domain/IDomainEvent.cs new file mode 100644 index 0000000..fdfafba --- /dev/null +++ a/src/Pole.Domain/IDomainEvent.cs @@ -0,0 +1,12 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain +{ + public interface IDomainEvent : IRequest + { + + } +} diff --git b/src/Pole.Domain/IDomainEventHandler.cs a/src/Pole.Domain/IDomainEventHandler.cs new file mode 100644 index 0000000..066fd5f --- /dev/null +++ a/src/Pole.Domain/IDomainEventHandler.cs @@ -0,0 +1,14 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pole.Domain +{ + public interface IDomainEventHandler : IRequestHandler where TCommand : IRequest + { + + } +} diff --git b/src/Pole.Domain/IRepository.cs a/src/Pole.Domain/IRepository.cs new file mode 100644 index 0000000..92d7e9a --- /dev/null +++ a/src/Pole.Domain/IRepository.cs @@ -0,0 +1,22 @@ +using Pole.Domain.UnitOfWork; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pole.Domain +{ + public interface IRepository : IRepository where T : AggregateRoot + { + void Update(T entity); + void Delete(T entity); + Task Add(T entity); + Task Get(string id); + IUnitOfWork UnitOfWork { get; } + } + public interface IRepository + { + + } +} diff --git b/src/Pole.Domain/Pole.Domain.csproj a/src/Pole.Domain/Pole.Domain.csproj new file mode 100644 index 0000000..f4aadb7 --- /dev/null +++ a/src/Pole.Domain/Pole.Domain.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git b/src/Pole.Domain/UnitOfWork/CompleteResult.cs a/src/Pole.Domain/UnitOfWork/CompleteResult.cs new file mode 100644 index 0000000..0586fa3 --- /dev/null +++ a/src/Pole.Domain/UnitOfWork/CompleteResult.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Domain.UnitOfWork +{ + public class CompleteResult + { + public static CompleteResult SuccessResult = new CompleteResult(1, "保存成功"); + public CompleteResult(int status, string message) + { + Status = status; + Message = message; + } + public CompleteResult(DomainHandleResult domainHandleResult) : this(domainHandleResult.Status, domainHandleResult.Message) { } + + /// + /// 1 Success 2 Faild ... + /// + public int Status { get;private set; } + public string Message { get;private set; } + } +} diff --git b/src/Pole.Domain/UnitOfWork/IUnitOfWork.cs a/src/Pole.Domain/UnitOfWork/IUnitOfWork.cs new file mode 100644 index 0000000..a5823da --- /dev/null +++ a/src/Pole.Domain/UnitOfWork/IUnitOfWork.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pole.Domain.UnitOfWork +{ + public interface IUnitOfWork : IDisposable + { + //Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task CompeleteAsync(CancellationToken cancellationToken = default); + } + +} diff --git b/src/Pole.Grpc/ExtraType/ProtoDecimal.cs a/src/Pole.Grpc/ExtraType/ProtoDecimal.cs new file mode 100644 index 0000000..ed9a25d --- /dev/null +++ a/src/Pole.Grpc/ExtraType/ProtoDecimal.cs @@ -0,0 +1,26 @@ +namespace Pole.Grpc.ExtraType +{ + public partial class ProtoDecimal + { + public ProtoDecimal(int v1, int v2, int v3, int v4) + { + V1 = v1; + V2 = v2; + V3 = v3; + V4 = v4; + } + + public static implicit operator decimal(ProtoDecimal protoDeciaml) => protoDeciaml.ToDecimal(); + + public static implicit operator ProtoDecimal(decimal value) => FromDecimal(value); + public decimal ToDecimal() + { + return new decimal(new int[] { V1, V2, V3, V4 }); + } + public static ProtoDecimal FromDecimal(decimal value) + { + var bits = decimal.GetBits(value); + return new ProtoDecimal(bits[0], bits[1], bits[2], bits[3]); + } + } +} diff --git b/src/Pole.Grpc/ExtraType/Protos/deciaml.proto a/src/Pole.Grpc/ExtraType/Protos/deciaml.proto new file mode 100644 index 0000000..d2c9f3d --- /dev/null +++ a/src/Pole.Grpc/ExtraType/Protos/deciaml.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + + +package pole.Grpc.ExtraType; + +message ProtoDecimal { + int32 v1 = 1; + int32 v2 = 2; + int32 v3 = 3; + int32 v4 = 4; +} \ No newline at end of file diff --git b/src/Pole.Grpc/Pole.Grpc.csproj a/src/Pole.Grpc/Pole.Grpc.csproj new file mode 100644 index 0000000..7de0efe --- /dev/null +++ a/src/Pole.Grpc/Pole.Grpc.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp3.0 + + + + + + + + + + diff --git b/src/Pole.Grpc/Validation/GrpcServiceOptionsExtensions.cs a/src/Pole.Grpc/Validation/GrpcServiceOptionsExtensions.cs new file mode 100644 index 0000000..43a5f9f --- /dev/null +++ a/src/Pole.Grpc/Validation/GrpcServiceOptionsExtensions.cs @@ -0,0 +1,17 @@ +using Grpc.AspNetCore.Server; +using Pole.Grpc.Validation; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class GrpcServiceOptionsExtensions + { + public static GrpcServiceOptions EnableMessageValidation(this GrpcServiceOptions options) + { + options.Interceptors.Add(); + return options; + } + } +} diff --git b/src/Pole.Grpc/Validation/IValidatorErrorMessageHandler.cs a/src/Pole.Grpc/Validation/IValidatorErrorMessageHandler.cs new file mode 100644 index 0000000..1e55e00 --- /dev/null +++ a/src/Pole.Grpc/Validation/IValidatorErrorMessageHandler.cs @@ -0,0 +1,14 @@ +using FluentValidation.Results; +using Grpc.Core; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Grpc.Validation +{ + public interface IValidatorErrorMessageHandler + { + Task HandleAsync(IList failures, ServerCallContext serverCallContext); + } +} diff --git b/src/Pole.Grpc/Validation/IValidatorProvider.cs a/src/Pole.Grpc/Validation/IValidatorProvider.cs new file mode 100644 index 0000000..be86769 --- /dev/null +++ a/src/Pole.Grpc/Validation/IValidatorProvider.cs @@ -0,0 +1,12 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Grpc.Validation +{ + public interface IValidatorProvider + { + bool TryGetValidator(out IValidator result) where TRequest : class; + } +} diff --git b/src/Pole.Grpc/Validation/IValidatorRegistrar.cs a/src/Pole.Grpc/Validation/IValidatorRegistrar.cs new file mode 100644 index 0000000..f7f18b7 --- /dev/null +++ a/src/Pole.Grpc/Validation/IValidatorRegistrar.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Grpc.Validation +{ + public interface IValidatorRegistrar + { + Task Register(Type validatorType,IServiceCollection services, ServiceLifetime lifetime= ServiceLifetime.Singleton); + } +} diff --git b/src/Pole.Grpc/Validation/Internal/DefaultValidatorErrorMessageHandler.cs a/src/Pole.Grpc/Validation/Internal/DefaultValidatorErrorMessageHandler.cs new file mode 100644 index 0000000..8a31912 --- /dev/null +++ a/src/Pole.Grpc/Validation/Internal/DefaultValidatorErrorMessageHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation.Results; +using Grpc.Core; + +namespace Pole.Grpc.Validation.Internal +{ + class DefaultValidatorErrorMessageHandler : IValidatorErrorMessageHandler + { + public Task HandleAsync(IList failures, ServerCallContext serverCallContext) + { + var requestPath= serverCallContext.GetHttpContext().Request.Path.Value; + + var errors = failures + .Select(f => $"Property {f.PropertyName} failed validation. Error was {f.ErrorMessage}. Request path was {requestPath}") + .ToList(); + var message = string.Join("\n", errors); + + serverCallContext.Status = new Status(StatusCode.InvalidArgument, message); + return Task.FromResult(1); + } + } +} diff --git b/src/Pole.Grpc/Validation/Internal/DefaultValidatorProvider.cs a/src/Pole.Grpc/Validation/Internal/DefaultValidatorProvider.cs new file mode 100644 index 0000000..6e54d7c --- /dev/null +++ a/src/Pole.Grpc/Validation/Internal/DefaultValidatorProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace Pole.Grpc.Validation.Internal +{ + class DefaultValidatorProvider : IValidatorProvider + { + private readonly IServiceProvider _provider; + + public DefaultValidatorProvider(IServiceProvider provider) + { + _provider = provider; + } + + public bool TryGetValidator(out IValidator result) where TRequest : class + { + result = _provider.GetService>(); + return result != null; + } + } +} diff --git b/src/Pole.Grpc/Validation/Internal/DefaultValidatorRegistrar.cs a/src/Pole.Grpc/Validation/Internal/DefaultValidatorRegistrar.cs new file mode 100644 index 0000000..0fd723b --- /dev/null +++ a/src/Pole.Grpc/Validation/Internal/DefaultValidatorRegistrar.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Grpc.Validation.Internal +{ + class DefaultValidatorRegistrar : IValidatorRegistrar + { + public Task Register(Type validatorType, IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + var messageType = validatorType.GetInterfaces().FirstOrDefault(t => + t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IValidator<>)).GetGenericArguments().First(); + var serviceType = typeof(IValidator<>).MakeGenericType(messageType); + + services.Add(new ServiceDescriptor(serviceType, validatorType, lifetime)); + return Task.FromResult(1); + } + } +} diff --git b/src/Pole.Grpc/Validation/ServiceCollectionExtensions.cs a/src/Pole.Grpc/Validation/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..da3f54f --- /dev/null +++ a/src/Pole.Grpc/Validation/ServiceCollectionExtensions.cs @@ -0,0 +1,48 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Pole.Grpc.Validation; +using Pole.Grpc.Validation.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddGrpcValidation(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + public static IServiceCollection AddGrpcRequestValidator(this IServiceCollection services,Assembly validatorAssembly ,ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + + Action validateOptionConfig = validateOption => { + validateOption.ValidatorAssembly = validatorAssembly; + }; + + services.Configure(validateOptionConfig); + + using (var serviceProvider= services.BuildServiceProvider()) + { + var option = serviceProvider.GetRequiredService>(); + var validatorRegistrar = serviceProvider.GetRequiredService(); + + var validators = option.Value.ValidatorAssembly.GetTypes().Where(m => m.GetInterfaces().FirstOrDefault(t => + t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IValidator<>))!=null); + foreach (var validator in validators) + { + validatorRegistrar.Register(validator, services); + } + + return services; + } + } + } +} diff --git b/src/Pole.Grpc/Validation/ValidateOption.cs a/src/Pole.Grpc/Validation/ValidateOption.cs new file mode 100644 index 0000000..4c5c286 --- /dev/null +++ a/src/Pole.Grpc/Validation/ValidateOption.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Pole.Grpc.Validation +{ + public class ValidateOption + { + public Assembly ValidatorAssembly { get; set; } + } +} diff --git b/src/Pole.Grpc/Validation/ValidationInterceptor.cs a/src/Pole.Grpc/Validation/ValidationInterceptor.cs new file mode 100644 index 0000000..e4ee3d3 --- /dev/null +++ a/src/Pole.Grpc/Validation/ValidationInterceptor.cs @@ -0,0 +1,43 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; +using Pole.Grpc.Validation; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Grpc.Validation +{ + public class ValidationInterceptor : Interceptor + { + private readonly IValidatorProvider _provider; + private readonly IValidatorErrorMessageHandler _handler; + public ValidationInterceptor(IValidatorProvider provider, IValidatorErrorMessageHandler handler) + { + _provider = provider; + _handler = handler; + } + + public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod continuation) + { + if (_provider.TryGetValidator(out var validator)) + { + var results = await validator.ValidateAsync(request); + + if (!results.IsValid) + { + await _handler.HandleAsync(results.Errors, context); + + var response= Expression.Lambda>(Expression.Block(typeof(TResponse), Expression.New(typeof(TResponse)))).Compile(); + return response(); + } + } + + return await continuation(request, context); + } + } +}