From dcf3b1020a210c46117fdc1e95ad569d6349a1f6 Mon Sep 17 00:00:00 2001 From: Tim Cliff Date: Thu, 1 Sep 2022 23:02:49 +0100 Subject: [PATCH] Add Exception middleware --- Component/Entries/Repository/Entry.cs | 18 +-- Component/Entries/Service/EntryMappings.cs | 3 +- Component/Entries/Service/EntryResources.cs | 2 +- Component/Entries/Service/EntryService.cs | 7 +- Data/DiaryDBContext.cs | 13 ++- Data/UnitOfWork.cs | 30 +++-- Diary.csproj | 7 +- Installers/DependencyInstallers.cs | 3 +- Installers/InstallExceptionsMiddleware.cs | 12 ++ Program.cs | 121 +++++++++++--------- Properties/launchSettings.json | 2 +- Shared/AppException.cs | 22 ++++ Shared/ExceptionDetails.cs | 31 +++++ Shared/ExceptionHandlingMiddleware.cs | 60 ++++++++++ 14 files changed, 241 insertions(+), 90 deletions(-) create mode 100644 Installers/InstallExceptionsMiddleware.cs create mode 100644 Shared/AppException.cs create mode 100644 Shared/ExceptionDetails.cs create mode 100644 Shared/ExceptionHandlingMiddleware.cs diff --git a/Component/Entries/Repository/Entry.cs b/Component/Entries/Repository/Entry.cs index 642f836..03d1a34 100644 --- a/Component/Entries/Repository/Entry.cs +++ b/Component/Entries/Repository/Entry.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Diary.Component.Entries.Repository { @@ -15,15 +10,14 @@ namespace Diary.Component.Entries.Repository public int EnrtyID { get; set; } [Required] - [Column(TypeName="date")] + [Column(TypeName = "date")] public DateTime Date { get; set; } [Required] - public DateTime ValidFrom { get; set; } + public DateTime ValidFrom { get; set; } = DateTime.Now; + public DateTime? ValidTo { get; set; } - public string Note { get; set; } - - + public string Note { get; set; } } -} +} \ No newline at end of file diff --git a/Component/Entries/Service/EntryMappings.cs b/Component/Entries/Service/EntryMappings.cs index 06241f7..78c5633 100644 --- a/Component/Entries/Service/EntryMappings.cs +++ b/Component/Entries/Service/EntryMappings.cs @@ -8,7 +8,8 @@ namespace Diary.Component.Entries.Service public EntryMappings() { CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date.Date)); } } } \ No newline at end of file diff --git a/Component/Entries/Service/EntryResources.cs b/Component/Entries/Service/EntryResources.cs index 6b4ad80..bdd6792 100644 --- a/Component/Entries/Service/EntryResources.cs +++ b/Component/Entries/Service/EntryResources.cs @@ -3,7 +3,7 @@ public class EntryResource { public int EnrtyID { get; set; } - public DateTime Date { get; set; } + public DateOnly Date { get; set; } public DateTime ValidFrom { get; set; } public DateTime? ValidTo { get; set; } diff --git a/Component/Entries/Service/EntryService.cs b/Component/Entries/Service/EntryService.cs index 6fa1fcf..ab28d80 100644 --- a/Component/Entries/Service/EntryService.cs +++ b/Component/Entries/Service/EntryService.cs @@ -1,5 +1,6 @@ using AutoMapper; using Diary.Component.Entries.Repository; +using Diary.Data; using System; using System.Collections.Generic; using System.Linq; @@ -18,11 +19,13 @@ namespace Diary.Component.Entries.Service { private readonly IEntryRepository _entryRepository; private readonly IMapper _mapper; + private readonly IUnitOfWork _unitOfWork; - public EntryService(IEntryRepository entryRepository, IMapper mapper) + public EntryService(IEntryRepository entryRepository, IMapper mapper, IUnitOfWork unitOfWork) { _entryRepository = entryRepository ?? throw new ArgumentNullException(nameof(entryRepository)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } @@ -32,6 +35,8 @@ namespace Diary.Component.Entries.Service await _entryRepository.CreateAsync(entry); + await _unitOfWork.CompleteAsync(); + return _mapper.Map(entry); } diff --git a/Data/DiaryDBContext.cs b/Data/DiaryDBContext.cs index 1148494..9f4eed3 100644 --- a/Data/DiaryDBContext.cs +++ b/Data/DiaryDBContext.cs @@ -3,17 +3,19 @@ using Microsoft.EntityFrameworkCore; namespace Diary.Data { - public interface IDiaryDBContext - { + { DbSet Entries { get; set; } + + Task SaveChangesAsync(CancellationToken cancellationToken = default); } public class DiaryDBContext : DbContext, IDiaryDBContext { public DbSet Entries { get; set; } - public DiaryDBContext(DbContextOptions options) : base(options){ + public DiaryDBContext(DbContextOptions options) : base(options) + { } protected override void OnModelCreating(ModelBuilder builder) @@ -22,5 +24,10 @@ namespace Diary.Data builder.ApplyConfiguration(new EntryConfiguration()); } + + public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } } } \ No newline at end of file diff --git a/Data/UnitOfWork.cs b/Data/UnitOfWork.cs index 1918b17..f2eebe9 100644 --- a/Data/UnitOfWork.cs +++ b/Data/UnitOfWork.cs @@ -1,17 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Diary.Data +namespace Diary.Data { - public class UnitOfWork - { - public UnitOfWork() - { + public interface IUnitOfWork + { + Task CompleteAsync(); + } + + public class UnitOfWork:IUnitOfWork + { + private readonly IDiaryDBContext _context; + + public UnitOfWork(IDiaryDBContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); } + public async Task CompleteAsync() + { + await _context.SaveChangesAsync(); + } } -} +} \ No newline at end of file diff --git a/Diary.csproj b/Diary.csproj index 0ad0a69..188849d 100644 --- a/Diary.csproj +++ b/Diary.csproj @@ -1,20 +1,23 @@ - + net6.0 enable - enable + true + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Installers/DependencyInstallers.cs b/Installers/DependencyInstallers.cs index 755638c..8f579c4 100644 --- a/Installers/DependencyInstallers.cs +++ b/Installers/DependencyInstallers.cs @@ -1,5 +1,6 @@ using Diary.Component.Entries.Repository; using Diary.Component.Entries.Service; +using Diary.Data; using System; using System.Collections.Generic; using System.Linq; @@ -12,7 +13,7 @@ namespace Diary.Installers { public static IServiceCollection AddDependencies(this IServiceCollection services) { - + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Installers/InstallExceptionsMiddleware.cs b/Installers/InstallExceptionsMiddleware.cs new file mode 100644 index 0000000..4882f36 --- /dev/null +++ b/Installers/InstallExceptionsMiddleware.cs @@ -0,0 +1,12 @@ +using Diary.Shared; + +namespace Diary.Installers +{ + public static class InstallExceptionsMiddleware + { + public static void AddExceptionsMiddleware(this IServiceCollection services) + { + services.AddScoped(); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index f1cb203..ea1d753 100644 --- a/Program.cs +++ b/Program.cs @@ -1,20 +1,26 @@ using Diary.Data; using Diary.Installers; +using Diary.Shared; using Serilog; using Serilog.Events; using System.Reflection; + + + + /* * Settings - Done by sefaulr * Logging - Done * Database - Donw * Dependencies - * automapper - Done + * Automapper - Done * Cors * Views/Filters/Validation * Swagger - Done * Auth - * + * Exception Middleware - Done + * Validation Filter * */ //+Setup Logger @@ -28,62 +34,65 @@ Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .CreateLogger(); +Log.Information("Starting up"); + +var builder = WebApplication.CreateBuilder(args); + +//---Y +//Add Serilog +builder.Host.UseSerilog(Log.Logger); +builder.Services.AddSingleton(new LoggingStartService(Log.Logger, Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().ManifestModule.ScopeName))); + +//---Y +//Database +builder.Services.AddDatabase(builder.Configuration); +builder.Services.AddScoped(); + +//---y +//Dependancies +builder.Services.AddDependencies(); + +//---Y +//Automapper +builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); + +builder.Services.AddControllers(); + +builder.Services.AddExceptionsMiddleware(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +//---R +//+Build + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(opt => + { + opt.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); + opt.RoutePrefix = String.Empty; + } + ); +} + +app.UseSerilogRequestLogging(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.UseMiddleware(); + try { - Log.Information("Starting up"); - - var builder = WebApplication.CreateBuilder(args); - - //---Y - //Add Serilog - builder.Host.UseSerilog(Log.Logger); - builder.Services.AddSingleton(new LoggingStartService(Log.Logger, Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().ManifestModule.ScopeName))); - - //---Y - //Database - builder.Services.AddDatabase(builder.Configuration); - builder.Services.AddScoped(); - - //---y - //Dependancies - builder.Services.AddDependencies(); - - - //---Y - //Automapper - builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); - - builder.Services.AddControllers(); - - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - //---R - //+Build - - var app = builder.Build(); - - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(opt => - { - opt.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); - opt.RoutePrefix = String.Empty; - } - ); - } - - app.UseSerilogRequestLogging(); - - app.UseHttpsRedirection(); - - app.UseAuthorization(); - - app.MapControllers(); - app.Run(); } catch (Exception ex) diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 1f1305a..1c2166c 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -6,7 +6,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Shared/AppException.cs b/Shared/AppException.cs new file mode 100644 index 0000000..4d8cd8d --- /dev/null +++ b/Shared/AppException.cs @@ -0,0 +1,22 @@ +using System.Net; + +namespace Diary.Shared +{ + public class AppException : Exception + { + public string Title { get; } + public HttpStatusCode StatusCode { get; } + + public AppException(HttpStatusCode statusCode, string title, string message) : base(message) + { + StatusCode = statusCode; + Title = title; + } + + public AppException(HttpStatusCode statusCode, string title, string message, Exception innerException) : base(message, innerException) + { + StatusCode = statusCode; + Title = title; + } + } +} \ No newline at end of file diff --git a/Shared/ExceptionDetails.cs b/Shared/ExceptionDetails.cs new file mode 100644 index 0000000..70b397a --- /dev/null +++ b/Shared/ExceptionDetails.cs @@ -0,0 +1,31 @@ +using System.Text.Json; + +namespace Diary.Shared +{ + public class ExceptionDetails + { + public string? Message { get; set; } + public string? Title { get; set; } + + public override string ToString() + { + var jsonSerializerSettings = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + return JsonSerializer.Serialize(this, jsonSerializerSettings); + } + } + + public class ValidationExceptionDetails : ExceptionDetails + { + public List ModelState { get; set; } = new(); + } + + public class ValidationProblemDescriptor + { + public string? Property { get; set; } + public string[]? Errors { get; set; } + } +} \ No newline at end of file diff --git a/Shared/ExceptionHandlingMiddleware.cs b/Shared/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..4d90dfd --- /dev/null +++ b/Shared/ExceptionHandlingMiddleware.cs @@ -0,0 +1,60 @@ + +namespace Diary.Shared +{ + public partial class ExceptionHandlingMiddleware : IMiddleware + { + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception e) + { + _logger.LogError(e, "Error received from ExceptionHandlingMiddleware"); + await HandleExceptionAsync(context, e); + } + } + + private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception) + { + ExceptionDetails exceptionDetails; + httpContext.Response.ContentType = "application/json"; + + if (exception.InnerException is AppException) + { + exception = exception.InnerException; + } + + if (exception is AppException applicationException) + { + httpContext.Response.StatusCode = (int)applicationException.StatusCode; + + exceptionDetails = new() + { + Message = applicationException.Message, + Title = applicationException.Title + }; + } + else + { + httpContext.Response.StatusCode = 500; + + exceptionDetails = new() + { + Message = exception.Message, + Title = "API Error" + }; + } + + await httpContext.Response.WriteAsync(exceptionDetails.ToString()); + } + } +} \ No newline at end of file