diff --git a/.gitignore b/.gitignore index 4b82ccd..f903760 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vs/ bin/ obj/ +/QRCodeService/Logs +.user \ No newline at end of file diff --git a/Domain/AggregateModel/AppAggregate/App.cs b/Domain/AggregateModel/AppAggregate/App.cs index 1b29f10..8c625ea 100644 --- a/Domain/AggregateModel/AppAggregate/App.cs +++ b/Domain/AggregateModel/AppAggregate/App.cs @@ -7,9 +7,8 @@ using System.Threading.Tasks; namespace Domain.AggregateModel.AppAggregate { - public class App : IAggregateRoot + public class App :Entity, IAggregateRoot { - public int Id { get; private set; } public string AppKey { get;private set; } public string BaseUrl { get; private set; } public string Remarks { get;private set; } diff --git a/Domain/AggregateModel/LinkAggregate/Link.cs b/Domain/AggregateModel/LinkAggregate/Link.cs index ee92e77..5233e91 100644 --- a/Domain/AggregateModel/LinkAggregate/Link.cs +++ b/Domain/AggregateModel/LinkAggregate/Link.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace Domain.AggregateModel.LinkAggregate { - public class Link:IAggregateRoot + public class Link:Entity,IAggregateRoot { /// /// 非自增主键 diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index 60a2896..27b30d8 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 diff --git a/Infrastructure/AppDbContext.cs b/Infrastructure/AppDbContext.cs new file mode 100644 index 0000000..bd251a6 --- /dev/null +++ b/Infrastructure/AppDbContext.cs @@ -0,0 +1,145 @@ +using Domain.AggregateModel.AppAggregate; +using Domain.AggregateModel.LinkAggregate; +using Domain.SeedWork; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Storage; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Infrastructure +{ + public class AppDbContext : DbContext, IUnitOfWork + { + public DbSet Apps { get; set; } + public DbSet Links { get; set; } + + private readonly IMediator _mediator; + private IDbContextTransaction _currentTransaction; + public bool HasActiveTransaction => _currentTransaction != null; + public IDbContextTransaction GetCurrentTransaction() => _currentTransaction; + + public AppDbContext(DbContextOptions options) : base(options) { } + public AppDbContext(DbContextOptions options, IMediator mediator) : base(options) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + + System.Diagnostics.Debug.WriteLine("OrderingContext::ctor ->" + this.GetHashCode()); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new CardTypeEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new OrderStatusEntityTypeConfiguration()); + //modelBuilder.ApplyConfiguration(new BuyerEntityTypeConfiguration()); + } + + + public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) + { + // Dispatch Domain Events collection. + // Choices: + // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including + // side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime + // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. + // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. + await _mediator.DispatchDomainEventsAsync(this); + + // After executing this line all the changes (from the Command Handler and Domain Event Handlers) + // performed through the DbContext will be committed + var result = await base.SaveChangesAsync(cancellationToken); + return true; + } + public async Task BeginTransactionAsync() + { + if (_currentTransaction != null) return null; + + _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); + + return _currentTransaction; + } + + public async Task CommitTransactionAsync(IDbContextTransaction transaction) + { + if (transaction == null) throw new ArgumentNullException(nameof(transaction)); + if (transaction != _currentTransaction) throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current"); + + try + { + await SaveChangesAsync(); + transaction.Commit(); + } + catch + { + RollbackTransaction(); + throw; + } + finally + { + if (_currentTransaction != null) + { + _currentTransaction.Dispose(); + _currentTransaction = null; + } + } + } + + public void RollbackTransaction() + { + try + { + _currentTransaction?.Rollback(); + } + finally + { + if (_currentTransaction != null) + { + _currentTransaction.Dispose(); + _currentTransaction = null; + } + } + } + } + public class AppDbContextDesignFactory : IDesignTimeDbContextFactory + { + public AppDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseMySql("Server=localhost;Database=qrcode;Uid=root;Pwd=root;"); + + return new AppDbContext(optionsBuilder.Options, new NoMediator()); + } + + class NoMediator : IMediator + { + public Task Publish(TNotification notification, CancellationToken cancellationToken = default(CancellationToken)) where TNotification : INotification + { + return Task.CompletedTask; + } + + public Task Publish(object notification, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task Send(IRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(default(TResponse)); + } + + public Task Send(object request, CancellationToken cancellationToken = default) + { + return Task.FromResult(default(object)); + } + } + } +} diff --git a/Infrastructure/EntityConfigurations/AppEntityTypeConfiguration.cs b/Infrastructure/EntityConfigurations/AppEntityTypeConfiguration.cs new file mode 100644 index 0000000..057118b --- /dev/null +++ b/Infrastructure/EntityConfigurations/AppEntityTypeConfiguration.cs @@ -0,0 +1,24 @@ +using Domain.AggregateModel.AppAggregate; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.EntityConfigurations +{ + public class AppEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("App"); + builder.HasKey(a => a.Id); + builder.Ignore(a => a.DomainEvents); + builder.Property(a => a.Id) + .ValueGeneratedOnAdd() + .IsRequired(); + } + } +} diff --git a/Infrastructure/EntityConfigurations/LinkEntityTypeConfiguration.cs b/Infrastructure/EntityConfigurations/LinkEntityTypeConfiguration.cs new file mode 100644 index 0000000..d3caaec --- /dev/null +++ b/Infrastructure/EntityConfigurations/LinkEntityTypeConfiguration.cs @@ -0,0 +1,22 @@ +using Domain.AggregateModel.LinkAggregate; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Infrastructure.EntityConfigurations +{ + public class LinkEntityTypeConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Link"); + builder.HasKey(l => l.ShortCode); + builder.Ignore(l => l.DomainEvents); + builder.Property(l => l.AppId).IsRequired(); + } + } +} diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index ee2d94c..0196593 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -1,9 +1,19 @@  - netstandard2.0 + netstandard2.1 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Infrastructure/MediatorExtension.cs b/Infrastructure/MediatorExtension.cs new file mode 100644 index 0000000..b41498f --- /dev/null +++ b/Infrastructure/MediatorExtension.cs @@ -0,0 +1,27 @@ +using Domain.SeedWork; +using MediatR; +using System.Linq; +using System.Threading.Tasks; + +namespace Infrastructure +{ + static class MediatorExtension + { + public static async Task DispatchDomainEventsAsync(this IMediator mediator, AppDbContext ctx) + { + 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) + await mediator.Publish(domainEvent); + } + } +} diff --git a/QRCodeService/Application/Behaviors/LoggingBehavior.cs b/QRCodeService/Application/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000..5995f78 --- /dev/null +++ b/QRCodeService/Application/Behaviors/LoggingBehavior.cs @@ -0,0 +1,24 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using QRCodeService.Extensions; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace QRCodeService.Application.Behaviors +{ + public class LoggingBehavior : IPipelineBehavior + { + private readonly ILogger> _logger; + public LoggingBehavior(ILogger> logger) => _logger = logger; + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + _logger.LogInformation("----- Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request); + var response = await next(); + _logger.LogInformation("----- Command {CommandName} handled - response: {@Response}", request.GetGenericTypeName(), response); + + return response; + } + } +} diff --git a/QRCodeService/Application/Behaviors/TransactionBehaviour.cs b/QRCodeService/Application/Behaviors/TransactionBehaviour.cs new file mode 100644 index 0000000..011ebdd --- /dev/null +++ b/QRCodeService/Application/Behaviors/TransactionBehaviour.cs @@ -0,0 +1,73 @@ +using Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using QRCodeService.Extensions; +using Serilog.Context; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace QRCodeService.Application.Behaviors +{ + public class TransactionBehaviour : IPipelineBehavior + { + private readonly ILogger> _logger; + private readonly AppDbContext _dbContext; + //private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; + + public TransactionBehaviour(AppDbContext dbContext, + //IOrderingIntegrationEventService orderingIntegrationEventService, + ILogger> logger) + { + _dbContext = dbContext ?? throw new ArgumentException(nameof(AppDbContext)); + //_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentException(nameof(orderingIntegrationEventService)); + _logger = logger ?? throw new ArgumentException(nameof(ILogger)); + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + var response = default(TResponse); + var typeName = request.GetGenericTypeName(); + + try + { + if (_dbContext.HasActiveTransaction) + { + return await next(); + } + + var strategy = _dbContext.Database.CreateExecutionStrategy(); + + await strategy.ExecuteAsync(async () => + { + Guid transactionId; + + using (var transaction = await _dbContext.BeginTransactionAsync()) + using (LogContext.PushProperty("TransactionContext", transaction.TransactionId)) + { + _logger.LogInformation("----- Begin transaction {TransactionId} for {CommandName} ({@Command})", transaction.TransactionId, typeName, request); + + response = await next(); + + _logger.LogInformation("----- Commit transaction {TransactionId} for {CommandName}", transaction.TransactionId, typeName); + + await _dbContext.CommitTransactionAsync(transaction); + + transactionId = transaction.TransactionId; + } + + //await _orderingIntegrationEventService.PublishEventsThroughEventBusAsync(transactionId); + }); + + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "ERROR Handling transaction for {CommandName} ({@Command})", typeName, request); + + throw; + } + } + } +} diff --git a/QRCodeService/Application/Behaviors/ValidatorBehavior.cs b/QRCodeService/Application/Behaviors/ValidatorBehavior.cs new file mode 100644 index 0000000..ef2a90b --- /dev/null +++ b/QRCodeService/Application/Behaviors/ValidatorBehavior.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using MediatR; +using Microsoft.Extensions.Logging; +using QRCodeService.Extensions; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace QRCodeService.Application.Behaviors +{ + public class ValidatorBehavior : IPipelineBehavior + { + private readonly ILogger> _logger; + private readonly IValidator[] _validators; + + public ValidatorBehavior(IValidator[] validators, ILogger> logger) + { + _validators = validators; + _logger = logger; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + var typeName = request.GetGenericTypeName(); + + _logger.LogInformation("----- Validating command {CommandType}", typeName); + + var failures = _validators + .Select(v => v.Validate(request)) + .SelectMany(result => result.Errors) + .Where(error => error != null) + .ToList(); + + if (failures.Any()) + { + _logger.LogWarning("Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}", typeName, request, failures); + + //throw new OrderingDomainException( + // $"Command Validation Errors for type {typeof(TRequest).Name}", new ValidationException("Validation exception", failures)); + } + + return await next(); + } + } +} \ No newline at end of file diff --git a/QRCodeService/Controllers/PlaygroundController.cs b/QRCodeService/Controllers/PlaygroundController.cs index 391339f..148b3fd 100644 --- a/QRCodeService/Controllers/PlaygroundController.cs +++ b/QRCodeService/Controllers/PlaygroundController.cs @@ -6,14 +6,22 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; - +using Microsoft.Extensions.Logging; namespace QRCodeService.Controllers { [Route("[controller]/[action]")] public class PlaygroundController:ControllerBase { + private readonly ILogger logger; + + public PlaygroundController(ILogger logger) + { + this.logger = logger; + } + public IActionResult GenShortCode(string url) { + logger.LogInformation("duduledule"); var codes = new string[2]; using (var md5 = MD5.Create()) { diff --git a/QRCodeService/Extensions/GenericTypeExtensions.cs b/QRCodeService/Extensions/GenericTypeExtensions.cs new file mode 100644 index 0000000..c748e57 --- /dev/null +++ b/QRCodeService/Extensions/GenericTypeExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QRCodeService.Extensions +{ + public static class GenericTypeExtensions + { + public static string GetGenericTypeName(this Type type) + { + var typeName = string.Empty; + + if (type.IsGenericType) + { + var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray()); + typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>"; + } + else + { + typeName = type.Name; + } + + return typeName; + } + + public static string GetGenericTypeName(this object @object) + { + return @object.GetType().GetGenericTypeName(); + } + } +} diff --git a/QRCodeService/Program.cs b/QRCodeService/Program.cs index 050fb85..af28239 100644 --- a/QRCodeService/Program.cs +++ b/QRCodeService/Program.cs @@ -1,26 +1,36 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Serilog; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.IO; namespace QRCodeService { public class Program { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + public static IConfiguration Configuration { get; } = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) + .AddJsonFile($"appsettings.{Environment.MachineName}.json", optional: true) + .AddEnvironmentVariables() + .Build(); public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .UseSerilog() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + + public static void Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(Configuration) + .CreateLogger(); + + CreateHostBuilder(args).Build().Run(); + } } -} +} \ No newline at end of file diff --git a/QRCodeService/QRCodeService.csproj b/QRCodeService/QRCodeService.csproj index 0b4421f..c860cfc 100644 --- a/QRCodeService/QRCodeService.csproj +++ b/QRCodeService/QRCodeService.csproj @@ -13,10 +13,18 @@ + - + + + + + + + + diff --git a/QRCodeService/QRCodeService.csproj.user b/QRCodeService/QRCodeService.csproj.user deleted file mode 100644 index 169ccc3..0000000 --- a/QRCodeService/QRCodeService.csproj.user +++ /dev/null @@ -1,11 +0,0 @@ - - - - ProjectDebugger - - - Docker - ApiControllerEmptyScaffolder - root/Common/Api - - \ No newline at end of file diff --git a/QRCodeService/Startup.cs b/QRCodeService/Startup.cs index 7f17ce6..62ef8cb 100644 --- a/QRCodeService/Startup.cs +++ b/QRCodeService/Startup.cs @@ -1,14 +1,21 @@ +using Infrastructure; +using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using Pomelo.EntityFrameworkCore.MySql; +using Pomelo.EntityFrameworkCore.MySql.Infrastructure; +using QRCodeService.Application.Behaviors; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; namespace QRCodeService @@ -31,6 +38,25 @@ namespace QRCodeService { c.SwaggerDoc("v1", new OpenApiInfo { Title = "QRCodeService", Version = "v1" }); }); + //MediatR + services.AddMediatR(Assembly.GetExecutingAssembly()); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehaviour<,>)); + + //EFCore + services.AddDbContextPool( + dbContextOptions => dbContextOptions + .UseMySql( + "server=localhost;user=root;password=root;database=qrcode", + // For common usages, see pull request #1233. + new MariaDbServerVersion(new Version(10, 5, 9)), // use MariaDbServerVersion for MariaDB + mySqlOptions => mySqlOptions + .CharSetBehavior(CharSetBehavior.NeverAppend)) + // Everything from this point on is optional but helps with debugging. + .EnableSensitiveDataLogging() + .EnableDetailedErrors() + ); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/QRCodeService/appsettings.json b/QRCodeService/appsettings.json index d9d9a9b..008de18 100644 --- a/QRCodeService/appsettings.json +++ b/QRCodeService/appsettings.json @@ -6,5 +6,38 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Serilog": { + "Using": [ "SeriLog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Async" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "Async", + "Args": { + "configure": [ + { + "Name": "File", + "Args": { + "path": "Logs/log.txt", + "rollingInterval": "Day" + } + } + ] + } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], + "Properties": { + "Application": "SerilogExample" + } + } }