├── KesselRun.Web.Api ├── appsettings.json ├── Infrastructure │ ├── Diagnostic │ │ ├── WebTraceEventIdentifiers.cs │ │ └── Tracing.cs │ ├── Mapping │ │ └── KesselRunApiProfile.cs │ ├── Bootstrapping │ │ ├── AppConfiguration.cs │ │ └── StartupConfigurer.cs │ ├── Invariable │ │ └── Config.cs │ └── Ioc │ │ └── DataAccessRegistry.cs ├── Messaging │ ├── Queries │ │ ├── GetUserQuery.cs │ │ ├── GetWeatherQuery.cs │ │ ├── GetTvShowQuery.cs │ │ └── GetColorsQuery.cs │ ├── Commands │ │ └── RegisterNewUserCommand.cs │ ├── QueryHandlers │ │ ├── GetColorsQueryHandler.cs │ │ ├── GetWeatherQueryHandler.cs │ │ ├── GetUserQueryHandler.cs │ │ └── GetTvShowQueryHandler.cs │ ├── CommandHandlers │ │ └── RegisterNewUserCommandHandler.cs │ └── Validation │ │ └── RegisterNewUserCommandValidator.cs ├── Properties │ └── launchSettings.json ├── serilogsettings.json ├── web.config ├── appsettings.Development.json ├── Controllers │ ├── V1.0 │ │ ├── UserController.cs │ │ ├── WeatherController.cs │ │ ├── ColorsController.cs │ │ ├── MediaContentController.cs │ │ └── RegisterUserController.cs │ └── V1.1 │ │ └── ColorsController.cs ├── HttpClients │ ├── TypedClientBase.cs │ ├── OpenMovieDbClient.cs │ └── WeatherClient.cs ├── KesselRun.Web.Api.csproj ├── Program.cs └── Startup.cs ├── KesselRunFramework.AspNet ├── Response │ ├── OpResult.cs │ ├── FailType.cs │ ├── BadRequest400Payload.cs │ ├── UnprocessableEntityPayload.cs │ ├── ApiResponse.cs │ └── OperationOutcome.cs ├── Infrastructure │ ├── HttpClient │ │ └── ITypedHttpClient.cs │ ├── ICurrentUser.cs │ ├── Bootstrapping │ │ ├── Config │ │ │ ├── HstsSettings.cs │ │ │ ├── GeneralConfig.cs │ │ │ ├── JwtSettings.cs │ │ │ ├── WebHostEnvironmentExtensions.cs │ │ │ ├── ApiBehaviourConfigurer.cs │ │ │ ├── SwaggerFilters │ │ │ │ ├── RemoveVersionFromParameterFilter.cs │ │ │ │ ├── ReplaceVersionWithExactValueInPath.cs │ │ │ │ └── SwaggerDefaultValuesFilter.cs │ │ │ ├── FluentValidationConfigurer.cs │ │ │ ├── MvcConfigurer.cs │ │ │ ├── JsonOptionsConfigurer.cs │ │ │ ├── RequestLoggingConfigurer.cs │ │ │ ├── ApplicationBuilderExtensions.cs │ │ │ ├── ServicesCollectionExtensions.cs │ │ │ ├── HttpClientConfigurer.cs │ │ │ ├── ConfigurationAddExtensions.cs │ │ │ └── VersioningExtensions.cs │ │ ├── Ioc │ │ │ ├── AppFrameworkRegistry.cs │ │ │ ├── JimmyBogardRegistry.cs │ │ │ └── ApplicationServicesRegistry.cs │ │ └── Common.cs │ ├── Invariants │ │ ├── Identity.cs │ │ ├── Browser.cs │ │ ├── Logging.cs │ │ ├── StartUpConfig.cs │ │ ├── Errors.cs │ │ ├── AspNet.cs │ │ └── Swagger.cs │ ├── Services │ │ ├── IRequestStateService.cs │ │ ├── ISessionStateService.cs │ │ ├── RequestStateService.cs │ │ └── SessionStateService.cs │ ├── Extensions │ │ ├── HostingEnvironmentExtensions.cs │ │ ├── ModelStateExtensions.cs │ │ ├── ControllerExtensions.cs │ │ ├── EnvironmentLoggerConfigurationExtensions.cs │ │ └── ValidateableResponseExtensions.cs │ ├── Controllers │ │ ├── AppApiLoggerController.cs │ │ ├── AppApiMediatrController.cs │ │ └── AppApiController.cs │ ├── Identity │ │ └── CurrentUserAdapter.cs │ ├── Logging │ │ ├── EventTypeEnricher.cs │ │ ├── TraceEventIdentifiers.cs │ │ └── LoggingExtensions.cs │ ├── ModelBinders │ │ ├── Providers │ │ │ └── DateTimeModelBinderProvider.cs │ │ └── UtcAwareDateTimeModelBinder.cs │ └── ActionFilters │ │ ├── SerilogMvcLoggingAttribute.cs │ │ ├── ValidatorActionFilterAttribute.cs │ │ └── ApiExceptionFilter.cs ├── Middleware │ ├── ApiExceptionOptions.cs │ ├── ApiExceptionMiddlewareExtensions.cs │ ├── OptionsDelegates.cs │ └── ApiExceptionMiddleware.cs ├── Messaging │ └── Pipelines │ │ ├── LogContextPipeline.cs │ │ ├── OperationProfilingPipeline.cs │ │ └── BusinessValidationPipeline.cs ├── Validation │ ├── SiteFluentValidatorFactory.cs │ ├── NullValidator.cs │ └── CompositeValidator.cs └── KesselRunFramework.AspNet.csproj ├── KesselRunFramework.DataAccess ├── Domain │ ├── IEntity.cs │ ├── INamedEntity.cs │ ├── ISimpleListItem.cs │ └── SimpleListItemObject.cs ├── ISqlDbConnectionManager.cs ├── KesselRunFramework.DataAccess.csproj ├── Infrastructure │ ├── IAdoNetUtilities.cs │ ├── Extensions │ │ ├── DbCommandExtensions.cs │ │ └── DataReaderExtensions.cs │ └── AdoNetUtilities.cs ├── IDbResolver.cs ├── Ops │ ├── ISimpleListRetriever.cs │ └── SimpleListRetriever.cs └── DbConnectionExtensions.cs ├── KesselRun.Business ├── DataTransferObjects │ ├── Weather │ │ ├── Clouds.cs │ │ ├── Coord.cs │ │ ├── Wind.cs │ │ ├── Rain.cs │ │ ├── Weather.cs │ │ ├── ForecastDto.cs │ │ ├── Sys.cs │ │ ├── City.cs │ │ ├── List.cs │ │ ├── Main.cs │ │ └── WeatherDto.cs │ ├── UserPayloadDto.cs │ ├── TvShowPayloadDto.cs │ ├── WeatherPayloadDto.cs │ ├── ColourPayloadDto.cs │ ├── RegisterUserPayloadDto.cs │ └── Media │ │ ├── TvShow.cs │ │ └── Episode.cs ├── ApplicationServices │ ├── IColoursService.cs │ └── ColorsService.cs ├── Validation │ ├── RegisterUserPayloadDtoValidator.cs │ └── ColorCollectionValidator.cs ├── Invariants │ └── Validation.cs └── KesselRun.Business.csproj ├── KesselRunFramework.Core ├── IApplicationService.cs ├── Infrastructure │ ├── Errors │ │ ├── ExceptionGravity.cs │ │ ├── EventIdIdentifier.cs │ │ ├── Api │ │ │ ├── ClientErrorPayload.cs │ │ │ ├── ApiValidationException.cs │ │ │ └── ApiException.cs │ │ └── EventIDs.cs │ ├── Http │ │ ├── ITypedClientResolver.cs │ │ ├── ProblemDetailTitles.cs │ │ ├── ProblemDetailTypes.cs │ │ └── TypedClientResolver.cs │ ├── Mapping │ │ ├── IMapFrom.cs │ │ ├── ICustomMapSomeClasses.cs │ │ ├── MapperProvider.cs │ │ └── ProfileBase.cs │ ├── Messaging │ │ ├── IValidateable.cs │ │ ├── IMembershipCommand.cs │ │ └── ValidateableResponse.cs │ ├── Extensions │ │ ├── DateTimeExtensions.cs │ │ ├── ListExtensions.cs │ │ ├── EnumerableExtensions.cs │ │ ├── GenericExtensions.cs │ │ ├── StreamExtensions.cs │ │ ├── StringExtensions.cs │ │ └── ValidationResultExtensions.cs │ ├── Invariants │ │ ├── GeneralPurpose.cs │ │ └── Validation.cs │ ├── Logging │ │ └── MessageTemplates.cs │ └── Validation │ │ └── Either.cs ├── IApplicationDataService.cs ├── ApplicationService.cs ├── ApplicationDataService.cs └── KesselRunFramework.Core.csproj ├── README.md ├── .github └── workflows │ └── dotnet.yml ├── LICENSE └── KesselRun.Examples.sln /KesselRun.Web.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AllowedHosts": "*" 3 | } 4 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/OpResult.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Response 2 | { 3 | public enum OpResult 4 | { 5 | Success = 0, 6 | Fail = 1 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Domain/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.DataAccess.Domain 2 | { 3 | public interface IEntity 4 | { 5 | public int Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Clouds.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Clouds 4 | { 5 | public long All { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Domain/INamedEntity.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.DataAccess.Domain 2 | { 3 | public interface INamedEntity : IEntity 4 | { 5 | string Name { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/UserPayloadDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects 2 | { 3 | public class UserPayloadDto 4 | { 5 | public string UserName { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/HttpClient/ITypedHttpClient.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.HttpClient 2 | { 3 | public interface ITypedHttpClient 4 | { 5 | // marker interface 6 | } 7 | } -------------------------------------------------------------------------------- /KesselRunFramework.Core/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace KesselRunFramework.Core 4 | { 5 | public interface IApplicationService 6 | { 7 | IMapper Mapper { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/ExceptionGravity.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Errors 2 | { 3 | public enum ExceptionGravity 4 | { 5 | Error, 6 | Critical 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/FailType.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Response 2 | { 3 | public enum FailType 4 | { 5 | None = 0, 6 | Error = 1, 7 | ValidationFailure = 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Diagnostic/WebTraceEventIdentifiers.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Web.Api.Infrastructure.Diagnostic 2 | { 3 | public enum WebTraceEventIdentifiers 4 | { 5 | RegisteringUserTrace = 100, 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Http/ITypedClientResolver.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Http 2 | { 3 | public interface ITypedClientResolver 4 | { 5 | T GetTypedClient() where T : class; 6 | } 7 | } -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Mapping/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Mapping 2 | { 3 | public interface IMapFrom 4 | { 5 | // Marker interface. No implementation by design. 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Messaging/IValidateable.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Messaging 2 | { 3 | public interface IValidateable 4 | { 5 | // marker interface. No members, by design. 6 | } 7 | } -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Domain/ISimpleListItem.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.DataAccess.Domain 2 | { 3 | public interface ISimpleListItem 4 | { 5 | int Id { get; set; } 6 | string Name { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Coord.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Coord 4 | { 5 | public double Lon { get; set; } 6 | public double Lat { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Wind.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Wind 4 | { 5 | public double Speed { get; set; } 6 | public long Deg { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Rain.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Rain 4 | { 5 | public double The1H { get; set; } 6 | public double The3H { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ICurrentUser.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure 2 | { 3 | public interface ICurrentUser 4 | { 5 | bool IsAuthenticated { get; } 6 | string UserName { get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/TvShowPayloadDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects 2 | { 3 | public class TvShowPayloadDto 4 | { 5 | public int Season { get; set; } 6 | public string Title { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/HstsSettings.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 2 | { 3 | public class HstsSettings 4 | { 5 | public double MaxAge { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/IApplicationDataService.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.DataAccess; 2 | 3 | namespace KesselRunFramework.Core 4 | { 5 | public interface IApplicationDataService 6 | { 7 | IDbResolver DbResolver { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/WeatherPayloadDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects 2 | { 3 | public class WeatherPayloadDto 4 | { 5 | public string City { get; set; } 6 | public string Units { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Domain/SimpleListItemObject.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.DataAccess.Domain 2 | { 3 | public class SimpleListItemObject : ISimpleListItem 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Mapping/ICustomMapSomeClasses.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Mapping 4 | { 5 | public interface ICustomMapSomeClasses 6 | { 7 | void CreateMappings(Profile profile); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/Identity.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class Identity 4 | { 5 | public const string Bearer = "Bearer"; 6 | public const string Oauth2 = "oauth2"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/ISqlDbConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Data.SqlClient; 3 | 4 | namespace KesselRunFramework.DataAccess 5 | { 6 | public interface ISqlDbConnectionManager : IDisposable 7 | { 8 | SqlConnection GetOpenConnection(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Queries/GetUserQuery.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Business.DataTransferObjects; 2 | using MediatR; 3 | 4 | namespace KesselRun.Web.Api.Messaging.Queries 5 | { 6 | public class GetUserQuery : IRequest 7 | { 8 | public int UserId { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/ColourPayloadDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects 2 | { 3 | public class ColorPayloadDto 4 | { 5 | public int Id { get; set; } 6 | public string Color { get; set; } 7 | public bool IsKnownColor { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/Browser.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class Browser 4 | { 5 | public sealed class CorsPolicy 6 | { 7 | public const string AllowAll = "All"; 8 | } 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/EventIdIdentifier.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Errors 2 | { 3 | public enum EventIdIdentifier 4 | { 5 | AppThrown = 1, 6 | UncaughtInAction = 2, 7 | UncaughtGlobal = 3, 8 | PipelineThrown = 4, 9 | HttpClient = 5 10 | } 11 | } -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Weather.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Weather 4 | { 5 | public long Id { get; set; } 6 | public string Main { get; set; } 7 | public string Description { get; set; } 8 | public string Icon { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/KesselRunFramework.DataAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Messaging/IMembershipCommand.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Messaging 2 | { 3 | public interface IMembershipCommand 4 | { 5 | // marker interface. No members, by design. 6 | // This type of command is for operations which will use a DbContext specific to user management. 7 | } 8 | } -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Queries/GetWeatherQuery.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Business.DataTransferObjects.Weather; 2 | using MediatR; 3 | 4 | namespace KesselRun.Web.Api.Messaging.Queries 5 | { 6 | public class GetWeatherQuery : IRequest 7 | { 8 | public string City { get; set; } 9 | public string Units { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Http/ProblemDetailTitles.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Http 2 | { 3 | public sealed class ProblemDetailTitles 4 | { 5 | public const string AuthorizationError = "An authorisation error has occurred."; 6 | public const string InternalServerError = "A server error has occurred."; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Extensions 4 | { 5 | public static class DateTimeExtensions 6 | { 7 | public static DateTime NextDay(this DateTime source) 8 | { 9 | return source.AddDays(1); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Invariants/GeneralPurpose.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Invariants 2 | { 3 | public sealed class GeneralPurpose 4 | { 5 | public const string AnonymousUser = "Anonymous"; 6 | public const string ColonDelimiter = ":"; 7 | public const string UniqueDelimiter = "_|_"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Services/IRequestStateService.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Services 2 | { 3 | public interface IRequestStateService 4 | { 5 | T GetItem(string key); 6 | bool HasItem(string key); 7 | bool RemoveItem(string key); 8 | void SetItem(string key, T item); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/BadRequest400Payload.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace KesselRunFramework.AspNet.Response 4 | { 5 | public class BadRequest400Payload 6 | { 7 | public string Title { get; } = "One or more validation errors occurred."; 8 | public IDictionary> Errors { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /KesselRunFramework.Core/ApplicationService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace KesselRunFramework.Core 4 | { 5 | public abstract class ApplicationService : IApplicationService 6 | { 7 | protected ApplicationService(IMapper mapper) 8 | { 9 | Mapper = mapper; 10 | } 11 | 12 | public IMapper Mapper { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Mapping/KesselRunApiProfile.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.Core.Infrastructure.Mapping; 2 | 3 | namespace KesselRun.Web.Api.Infrastructure.Mapping 4 | { 5 | public class KesselRunApiProfile : ProfileBase 6 | { 7 | public KesselRunApiProfile(string profileName) 8 | : base(profileName) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Http/ProblemDetailTypes.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Http 2 | { 3 | public sealed class ProblemDetailTypes 4 | { 5 | public const string InternalServerError = "https://tools.ietf.org/html/rfc7231#section-6.6.1"; 6 | public const string Unauthorized = "https://tools.ietf.org/html/rfc7235#section-3.1"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Extensions 4 | { 5 | public static class ListExtensions 6 | { 7 | public static void Add(this List source, IEnumerable items) 8 | { 9 | source.AddRange(items); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/ForecastDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class ForecastDto 4 | { 5 | public long Cod { get; set; } 6 | public long Message { get; set; } 7 | public long Cnt { get; set; } 8 | public List[] List { get; set; } 9 | public City City { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/UnprocessableEntityPayload.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace KesselRunFramework.AspNet.Response 4 | { 5 | public class UnprocessableEntityPayload 6 | { 7 | public string Title { get; } = "One or more validation errors occurred."; 8 | public IDictionary> Errors { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/Logging.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class Logging 4 | { 5 | public sealed class LogContexts 6 | { 7 | public const string RequestTypeProperty = "RequestType"; 8 | public const string FromRoute = "From Route"; 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/GeneralConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 5 | { 6 | public class GeneralConfig 7 | { 8 | public string TimeZoneCity { get; set; } 9 | public IList OpenApiInfoList { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace KesselRunFramework.Core.Infrastructure.Extensions 5 | { 6 | public static class EnumerableExtensions 7 | { 8 | public static bool None(this IEnumerable source) 9 | { 10 | return !source.Any(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/RegisterUserPayloadDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRun.Business.DataTransferObjects 4 | { 5 | public class RegisterUserPayloadDto 6 | { 7 | public string UserName { get; set; } 8 | public string Password { get; set; } 9 | public string ConfirmPassword { get; set; } 10 | public DateTime DateOfBirth { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Sys.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Sys 4 | { 5 | public string Pod { get; set; } 6 | 7 | public long Type { get; set; } 8 | public long Id { get; set; } 9 | public string Country { get; set; } 10 | public long Sunrise { get; set; } 11 | public long Sunset { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/JwtSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 4 | { 5 | public sealed class JwtSettings 6 | { 7 | public double ClockSkew { get; set; } 8 | public IEnumerable Issuers { get; set; } 9 | public string CertificateThumbprint { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Invariants/Validation.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Invariants 2 | { 3 | public sealed class Validation 4 | { 5 | public sealed class PartMessages 6 | { 7 | public const string MustNotBeEmpty = "must not be empty."; 8 | public const string NotValidEmailAddress = "is not a valid email address."; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ServerFramework 2 | A small collection of libraries, opinionated in nature, which cut down on the amount of set-up code you need to write when starting a new ASP.NET Core Web Application. 3 | 4 | This repository will serve as an example application to underpin various articles which I intend to write about ASP.NET Core and some of the great libraries which you can use to build up a server-side framework to support your projects. 5 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Bootstrapping/AppConfiguration.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config; 2 | 3 | namespace KesselRun.Web.Api.Infrastructure.Bootstrapping 4 | { 5 | public class AppConfiguration 6 | { 7 | public GeneralConfig GeneralConfig { get; set; } 8 | public HstsSettings HstsSettings { get; set; } 9 | public JwtSettings JwtSettings { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Infrastructure/IAdoNetUtilities.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System.Collections.Generic; 3 | 4 | namespace KesselRunFramework.DataAccess.Infrastructure 5 | { 6 | public interface IAdoNetUtilities 7 | { 8 | string AddParametersToCommand( 9 | SqlCommand sqlCommand, 10 | IEnumerable ids, 11 | string paramPrefix = null 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRun.Business/ApplicationServices/IColoursService.cs: -------------------------------------------------------------------------------- 1 | using BusinessValidation; 2 | using KesselRun.Business.DataTransferObjects; 3 | using KesselRunFramework.Core.Infrastructure.Validation; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | namespace KesselRun.Business.ApplicationServices 8 | { 9 | public interface IColorsService 10 | { 11 | Task, Validator>> GetColorsAsync(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Invariable/Config.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Web.Api.Infrastructure.Invariable 2 | { 3 | public sealed class Config 4 | { 5 | public const string ApplicationConfigFromJson = "Application"; 6 | 7 | public sealed class Files 8 | { 9 | public const string SerilogBaseConfig = "serilogsettings.json"; 10 | public const string SerilogConfig = "serilogsettings.{0}.json"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Queries/GetTvShowQuery.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Business.DataTransferObjects.Media; 2 | using KesselRunFramework.Core.Infrastructure.Validation; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace KesselRun.Web.Api.Messaging.Queries 7 | { 8 | public class GetTvShowQuery : IRequest> 9 | { 10 | public int Season { get; set; } 11 | public string Title { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/StartUpConfig.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class StartUpConfig 4 | { 5 | public const string Domain = "DomainAssembly"; 6 | public const string Executing = "ExecutingAssembly"; 7 | public const string MediatR = "MediatR"; 8 | 9 | public const int MajorVersion1 = 1; 10 | public const int MinorVersion0 = 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Infrastructure/Extensions/DbCommandExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System.Data; 3 | 4 | namespace KesselRunFramework.DataAccess.Infrastructure.Extensions 5 | { 6 | public static class DbCommandExtensions 7 | { 8 | public static void AddIdParameter(this SqlCommand command, string paramName, int id) 9 | { 10 | command.Parameters.Add(paramName, SqlDbType.Int).Value = id; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Media/TvShow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace KesselRun.Business.DataTransferObjects.Media 5 | { 6 | public class TvShow 7 | { 8 | public string Title { get; set; } 9 | public int Season { get; set; } 10 | public int TotalSeasons { get; set; } 11 | public string Response { get; set; } 12 | 13 | 14 | public ICollection Episodes { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Extensions/HostingEnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Extensions 5 | { 6 | public static class HostingEnvironmentExtensions 7 | { 8 | public static bool IsDevelopmentOrStaging(this IWebHostEnvironment env) 9 | { 10 | return env.IsDevelopment() || env.IsStaging(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/IDbResolver.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.DataAccess 2 | { 3 | public interface IDbResolver 4 | { 5 | /// 6 | /// This method resolves the context (application-specific) and returns it. 7 | /// 8 | /// 9 | /// 10 | T GetContext() where T : class; 11 | 12 | ISqlDbConnectionManager GetDbConnectionManager(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Media/Episode.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace KesselRun.Business.DataTransferObjects.Media 4 | { 5 | public class Episode 6 | { 7 | public string Title { get; set; } 8 | public string Released { get; set; } 9 | [JsonPropertyName("episode")] 10 | public int EpisodeNr { get; set; } 11 | public string ImdbRating { get; set; } 12 | public string ImdbID { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Middleware/ApiExceptionOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using KesselRunFramework.AspNet.Response; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace KesselRunFramework.AspNet.Middleware 7 | { 8 | public class ApiExceptionOptions 9 | { 10 | public Action AddResponseDetails { get; set; } 11 | public Func DetermineLogLevel { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Commands/RegisterNewUserCommand.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Business.DataTransferObjects; 2 | using KesselRunFramework.AspNet.Response; 3 | using KesselRunFramework.Core.Infrastructure.Messaging; 4 | using MediatR; 5 | 6 | namespace KesselRun.Web.Api.Messaging.Commands 7 | { 8 | public class RegisterNewUserCommand : IRequest>>, IValidateable, IMembershipCommand 9 | { 10 | public RegisterUserPayloadDto Dto { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Queries/GetColorsQuery.cs: -------------------------------------------------------------------------------- 1 | using BusinessValidation; 2 | using KesselRun.Business.DataTransferObjects; 3 | using KesselRunFramework.Core.Infrastructure.Validation; 4 | using MediatR; 5 | using System.Collections.Generic; 6 | 7 | namespace KesselRun.Web.Api.Messaging.Queries 8 | { 9 | public class GetColorsQuery : IRequest, Validator>> 10 | { 11 | // no properties necessary, as the query will return all Colors. 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/GenericExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Extensions 4 | { 5 | public static class GenericExtensions 6 | { 7 | public static T[] InArray(this T item) 8 | { 9 | return new[] { item }; 10 | } 11 | 12 | public static IList InList(this T item) 13 | { 14 | return new List { item }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Controllers/AppApiLoggerController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Controllers 4 | { 5 | public class AppApiLoggerController : AppApiController 6 | { 7 | protected readonly ILogger _logger; 8 | 9 | public AppApiLoggerController(ICurrentUser currentUser, ILogger logger) 10 | : base(currentUser) 11 | { 12 | _logger = logger; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/ApplicationDataService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using KesselRunFramework.DataAccess; 3 | 4 | namespace KesselRunFramework.Core 5 | { 6 | public abstract class ApplicationDataService : ApplicationService, IApplicationDataService 7 | { 8 | protected ApplicationDataService(IDbResolver dbResolver, IMapper mapper) 9 | : base(mapper) 10 | { 11 | DbResolver = dbResolver; 12 | } 13 | 14 | public IDbResolver DbResolver { get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/Api/ClientErrorPayload.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Errors.Api 2 | { 3 | /// 4 | /// An response returned to the client 5 | /// 6 | public class ClientErrorPayload 7 | { 8 | public string Message { get; set; } 9 | public bool IsError { get; set; } 10 | public bool IsValidationFail { get; set; } 11 | public string Detail { get; set; } 12 | public object Data { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/City.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public partial class City 4 | { 5 | public long Id { get; set; } 6 | public string Name { get; set; } 7 | public Coord Coord { get; set; } 8 | public string Country { get; set; } 9 | public long Population { get; set; } 10 | public long Timezone { get; set; } 11 | public long Sunrise { get; set; } 12 | public long Sunset { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/WebHostEnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 5 | { 6 | public static class WebHostEnvironmentExtensions 7 | { 8 | public static bool IsDevelopmentOrIsStaging(this IWebHostEnvironment webHostEnvironment) 9 | { 10 | return webHostEnvironment.IsDevelopment() || webHostEnvironment.IsStaging(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/List.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRun.Business.DataTransferObjects.Weather 4 | { 5 | public partial class List 6 | { 7 | public long Dt { get; set; } 8 | public Main Main { get; set; } 9 | public Weather[] Weather { get; set; } 10 | public Clouds Clouds { get; set; } 11 | public Wind Wind { get; set; } 12 | public Sys Sys { get; set; } 13 | public DateTimeOffset DtTxt { get; set; } 14 | public Rain Rain { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Logging/MessageTemplates.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.Core.Infrastructure.Logging 2 | { 3 | public class MessageTemplates 4 | { 5 | public const string DefaultLog = "{ErrorMessage} -- {ErrorId}."; 6 | public const string HttpClientGet = "GET request to {@AbsolutePath}"; 7 | public const string UncaughtGlobal = "Uncaught Exception. {ResolvedExceptionMessage} -- {ErrorId}."; 8 | public const string ValidationErrorsLog = "Validation Fail. {Errors}. User Id {UserName}."; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Controllers/AppApiMediatrController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Controllers 5 | { 6 | public class AppApiMediatrController : AppApiLoggerController 7 | { 8 | protected readonly IMediator _mediator; 9 | 10 | public AppApiMediatrController(ICurrentUser currentUser, ILogger logger, IMediator mediator) 11 | : base(currentUser, logger) 12 | { 13 | _mediator = mediator; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/Main.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class Main 4 | { 5 | public double Temp { get; set; } 6 | public double FeelsLike { get; set; } 7 | public double TempMin { get; set; } 8 | public long TempMax { get; set; } 9 | public long Pressure { get; set; } 10 | public long SeaLevel { get; set; } 11 | public long GrndLevel { get; set; } 12 | public long Humidity { get; set; } 13 | public double TempKf { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Http/TypedClientResolver.cs: -------------------------------------------------------------------------------- 1 | using SimpleInjector; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Http 4 | { 5 | public class TypedClientResolver : ITypedClientResolver 6 | { 7 | private readonly Container _container; 8 | 9 | public TypedClientResolver(Container container) 10 | { 11 | _container = container; 12 | } 13 | 14 | public T GetTypedClient() 15 | where T : class 16 | { 17 | return _container.GetInstance(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/Errors.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class Errors 4 | { 5 | public const string UnhandledErrorDebug = @"An unhandled error occurred. {0}"; 6 | public const string UnhandledError = @"An error has occurred in the Web API. " + 7 | "Please contact our support team if the problem persists, citing this Error Id: {0}"; 8 | public const string ValidationFailure = @"Validation errors have occurred at the server."; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Bootstrapping/StartupConfigurer.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Web.Api.Infrastructure.Invariable; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace KesselRun.Web.Api.Infrastructure.Bootstrapping 5 | { 6 | public static class StartupConfigurer 7 | { 8 | public static AppConfiguration GetAppConfiguration(IConfiguration configuration) 9 | { 10 | var AppConfiguration = new AppConfiguration(); 11 | configuration.Bind(Config.ApplicationConfigFromJson, AppConfiguration); 12 | 13 | return AppConfiguration; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace KesselRunFramework.AspNet.Response 4 | { 5 | public class ApiResponse : ApiResponse 6 | { 7 | public T Data { get; set; } 8 | } 9 | 10 | public class ApiResponse 11 | { 12 | public ApiResponse() 13 | { 14 | Outcome = new OperationOutcome 15 | { 16 | Errors = Enumerable.Empty(), 17 | Message = string.Empty, 18 | OpResult = OpResult.Success // optimistic 🤞 19 | }; 20 | } 21 | public OperationOutcome Outcome { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KesselRun.Business/Validation/RegisterUserPayloadDtoValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using KesselRun.Business.DataTransferObjects; 3 | 4 | namespace KesselRun.Business.Validation 5 | { 6 | public class RegisterUserPayloadDtoValidator : AbstractValidator 7 | { 8 | public RegisterUserPayloadDtoValidator() 9 | { 10 | RuleFor(p => p.Password).NotEmpty(); // defer password strength validation to Identity library 11 | RuleFor(p => p.ConfirmPassword) 12 | .NotEmpty() 13 | .Equal(p => p.Password); 14 | RuleFor(p => p.UserName).NotEmpty(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/ApiBehaviourConfigurer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 4 | { 5 | public class ApiBehaviourConfigurer 6 | { 7 | public static void ConfigureApiBehaviour(ApiBehaviorOptions apiBehaviorOptions) 8 | { 9 | apiBehaviorOptions.InvalidModelStateResponseFactory = context => 10 | { 11 | var payload = Common.ProcessInvalidModelState(context.ModelState); 12 | 13 | return new BadRequestObjectResult(payload); 14 | }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/SwaggerFilters/RemoveVersionFromParameterFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config.SwaggerFilters 6 | { 7 | public class RemoveVersionFromParameterFilter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 10 | { 11 | var versionParameter = operation.Parameters.Single(p => p.Name == "version"); 12 | operation.Parameters.Remove(versionParameter); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Extensions/ModelStateExtensions.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping; 2 | using KesselRunFramework.AspNet.Response; 3 | using Microsoft.AspNetCore.Mvc.ModelBinding; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Extensions 6 | { 7 | public static class ModelStateExtensions 8 | { 9 | public static UnprocessableEntityPayload ToUnprocessablePayload(this ModelStateDictionary source) 10 | { 11 | var errors = Common.GetErrorsFromModelState(source); 12 | 13 | return new UnprocessableEntityPayload 14 | { 15 | Errors = errors 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/FluentValidationConfigurer.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.AspNetCore; 2 | using KesselRunFramework.AspNet.Validation; 3 | using SimpleInjector; 4 | using System; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 7 | { 8 | public class FluentValidationConfigurer 9 | { 10 | public static Action ConfigureFluentValidation(Container container) 11 | { 12 | return fluentValidationMvcConfiguration => 13 | { 14 | fluentValidationMvcConfiguration.ValidatorFactory = new SiteFluentValidatorFactory(container); 15 | }; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /KesselRun.Business/Invariants/Validation.cs: -------------------------------------------------------------------------------- 1 | using FrmwkInvariants = KesselRunFramework.Core.Infrastructure.Invariants; 2 | 3 | namespace KesselRun.Business.Invariants 4 | { 5 | public sealed class Validation 6 | { 7 | public sealed class PartMessages 8 | { 9 | public const string UserAlreadyExists = "already exists as a user."; 10 | 11 | } 12 | 13 | public sealed class Messages 14 | { 15 | public const string UserNameMustNotBeEmpty = "UserName " + FrmwkInvariants.Validation.PartMessages.MustNotBeEmpty; 16 | public const string UserRoleMustNotBeEmpty = "User Role " + FrmwkInvariants.Validation.PartMessages.MustNotBeEmpty; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/MvcConfigurer.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Infrastructure.ActionFilters; 2 | using KesselRunFramework.AspNet.Infrastructure.ModelBinders.Providers; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 6 | { 7 | public class MvcConfigurer 8 | { 9 | public static void ConfigureMvcOptions(MvcOptions mvcOptions) 10 | { 11 | mvcOptions.Filters.Add(typeof(SerilogMvcLoggingAttribute)); 12 | mvcOptions.Filters.Add(typeof(ApiExceptionFilter)); 13 | mvcOptions.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider()); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /KesselRun.Business/KesselRun.Business.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Services/ISessionStateService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Services 4 | { 5 | public interface ISessionStateService 6 | { 7 | bool? GetBoolean(string key); 8 | DateTime? GetDateTime(string key); 9 | DateTimeOffset? GetDateTimeOffset(string key); 10 | int? GetInt(string key); 11 | T GetObject(string key); 12 | bool HasItem(string key); 13 | void SetBoolean(string key, bool value); 14 | void SetDateTime(string key, DateTime value); 15 | void SetDateTimeOffset(string key, DateTimeOffset value); 16 | 17 | void SetInt(string key, int value); 18 | void SetObject(string key, T item); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KesselRun.Business/DataTransferObjects/Weather/WeatherDto.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRun.Business.DataTransferObjects.Weather 2 | { 3 | public class WeatherDto 4 | { 5 | public Coord Coord { get; set; } 6 | public KesselRun.Business.DataTransferObjects.Weather.Weather[] Weather { get; set; } 7 | public string Base { get; set; } 8 | public Main Main { get; set; } 9 | public long Visibility { get; set; } 10 | public Wind Wind { get; set; } 11 | public Clouds Clouds { get; set; } 12 | public long Dt { get; set; } 13 | public Sys Sys { get; set; } 14 | public long Timezone { get; set; } 15 | public long Id { get; set; } 16 | public string Name { get; set; } 17 | public long Cod { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Identity/CurrentUserAdapter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Identity 4 | { 5 | public class CurrentUserAdapter : ICurrentUser 6 | { 7 | protected readonly IHttpContextAccessor _httpContextAccessor; 8 | protected readonly HttpContext _httpContext; 9 | 10 | public CurrentUserAdapter(IHttpContextAccessor httpContextAccessor) 11 | { 12 | _httpContextAccessor = httpContextAccessor; 13 | _httpContext = _httpContextAccessor.HttpContext; 14 | } 15 | 16 | public virtual bool IsAuthenticated => _httpContext.User.Identity.IsAuthenticated; 17 | public virtual string UserName => _httpContext.User.Identity.Name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | dotnet-version: [ '6.0.x' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup dotnet ${{ matrix.dotnet-version }} 20 | uses: actions/setup-dotnet@v2 21 | with: 22 | dotnet-version: ${{ matrix.dotnet-version }} 23 | - name: Display dotnet version 24 | run: dotnet --version 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --no-restore 29 | - name: Test 30 | run: dotnet test --no-build --verbosity normal 31 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Ops/ISimpleListRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using KesselRunFramework.DataAccess.Domain; 5 | using Microsoft.Data.SqlClient; 6 | 7 | namespace KesselRunFramework.DataAccess.Ops 8 | { 9 | public interface ISimpleListRetriever 10 | { 11 | IEnumerable GetSimpleList( 12 | string query, 13 | SqlParameter[] parameters = null, 14 | bool? withNullCheck = null 15 | ); 16 | Task> GetSimpleListAsync( 17 | string query, 18 | CancellationToken cancellationToken, 19 | SqlParameter[] parameters = null, 20 | bool? withNullCheck = null 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KesselRun.Business/Validation/ColorCollectionValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using KesselRun.Business.DataTransferObjects; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace KesselRun.Business.Validation 7 | { 8 | public class ColorCollectionValidator : AbstractValidator> 9 | { 10 | public ColorCollectionValidator() 11 | { 12 | // This is a bit of a silly validator. 13 | // Normally a collection would be a property on a DTO. 14 | // But it interesting to see that you can do this with FluentValidation at all. 15 | RuleFor(c => c) 16 | .Must(c => c.Any()) 17 | .WithMessage("The color collection must contain at least one color."); 18 | 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Messaging/Pipelines/LogContextPipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 4 | using MediatR; 5 | using Serilog.Context; 6 | 7 | namespace KesselRunFramework.AspNet.Messaging.Pipelines 8 | { 9 | public class LogContextPipeline : IPipelineBehavior 10 | where TRequest : IRequest 11 | { 12 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 13 | { 14 | using (LogContext.PushProperty(Logging.LogContexts.RequestTypeProperty, typeof(TRequest).Name)) 15 | { 16 | return await next(); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/KesselRunFramework.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Logging/EventTypeEnricher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Murmur; 4 | using Serilog.Core; 5 | using Serilog.Events; 6 | 7 | namespace KesselRunFramework.AspNet.Infrastructure.Logging 8 | { 9 | public class EventTypeEnricher : ILogEventEnricher 10 | { 11 | public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) 12 | { 13 | var murmur = MurmurHash.Create32(); 14 | var bytes = Encoding.UTF8.GetBytes(logEvent.MessageTemplate.Text); 15 | var hash = murmur.ComputeHash(bytes); 16 | var numericHash = BitConverter.ToUInt32(hash, 0); 17 | var eventId = propertyFactory.CreateProperty("EventType", numericHash); 18 | logEvent.AddPropertyIfAbsent(eventId); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Ioc/DataAccessRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using KesselRunFramework.DataAccess.Ops; 3 | using SimpleInjector; 4 | 5 | namespace KesselRun.Web.Api.Infrastructure.Ioc 6 | { 7 | public static class DataAccessRegistry 8 | { 9 | public static void RegisterDataAccessComponents(this Container container, string connectionString) 10 | { 11 | if (container == null) throw new ArgumentNullException(nameof(container)); 12 | if (connectionString == null) throw new ArgumentNullException(nameof(connectionString)); 13 | if (connectionString.Trim().Equals(string.Empty)) throw new ArgumentException(nameof(connectionString) + " cannot be an empty string."); 14 | 15 | container.Register(Lifestyle.Singleton); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Logging/TraceEventIdentifiers.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Logging 2 | { 3 | public enum TraceEventIdentifiers 4 | { 5 | ProfileMessagingTrace = 50, 6 | BeforeValidatingMessageTrace = 51, 7 | InValidMessageTrace = 52, 8 | ValidMessageTrace = 53, 9 | ModelBinderUsedTrace = 54, 10 | AttemptingToBindModelTrace = 55, 11 | AttemptingToBindParameterModelTrace = 56, 12 | AttemptingToBindPropertyModelTrace = 57, 13 | FoundNoValueInRequestTrace = 58, 14 | FoundNoValueForParameterInRequestTrace = 59, 15 | FoundNoValueForPropertyInRequestTrace = 60, 16 | DoneAttemptingToBindModelTrace = 61, 17 | DoneAttemptingToBindParameterModelTrace = 62, 18 | DoneAttemptingToBindPropertyModelTrace = 63 19 | } 20 | } -------------------------------------------------------------------------------- /KesselRun.Web.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:50933", 7 | "sslPort": 44329 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "KesselRun.Web.Api": { 20 | "commandName": "Project", 21 | "launchUrl": "weatherforecast", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Middleware/ApiExceptionMiddlewareExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace KesselRunFramework.AspNet.Middleware 5 | { 6 | public static class ApiExceptionMiddlewareExtensions 7 | { 8 | public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder) 9 | { 10 | var options = new ApiExceptionOptions(); 11 | return builder.UseMiddleware(options); 12 | } 13 | 14 | public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder, 15 | Action configureOptions) 16 | { 17 | var options = new ApiExceptionOptions(); 18 | configureOptions(options); 19 | 20 | return builder.UseMiddleware(options); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/serilogsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "KesselRunFramework.AspNet" ], 4 | "Enrich": [ "FromLogContext", "WithEventType" ], 5 | "MinimumLevel": { 6 | "Default": "Verbose" 7 | }, 8 | "WriteTo": [ 9 | { 10 | "Name": "Console", 11 | "Args": { 12 | "restrictedToMinimumLevel": "Debug" 13 | } 14 | }, 15 | { 16 | "Name": "File", 17 | "Args": { 18 | "path": "Logs/apilog.log", 19 | "fileSizeLimitBytes": 1073741824, 20 | "formatter": "Serilog.Formatting.Json.JsonFormatter", 21 | "retainedFileCountLimit": 30, 22 | "rollingInterval": "Day", 23 | "rollOnFileSizeLimit": true, 24 | "restrictedToMinimumLevel": "Debug" 25 | } 26 | } 27 | ], 28 | "Properties": { 29 | "Application": "KesselRun Reference Application" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/EventIDs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Errors 4 | { 5 | public static class EventIDs 6 | { 7 | public static readonly EventId EventIdAppThrown = new EventId((int)EventIdIdentifier.AppThrown, EventIdIdentifier.AppThrown.ToString()); 8 | public static readonly EventId EventIdHttpClient = new EventId((int)EventIdIdentifier.HttpClient, EventIdIdentifier.HttpClient.ToString()); 9 | public static readonly EventId EventIdPipelineThrown = new EventId((int)EventIdIdentifier.PipelineThrown, EventIdIdentifier.PipelineThrown.ToString()); 10 | public static readonly EventId EventIdUncaught = new EventId((int)EventIdIdentifier.UncaughtInAction, EventIdIdentifier.UncaughtInAction.ToString()); 11 | public static readonly EventId EventIdUncaughtGlobal = new EventId((int)EventIdIdentifier.UncaughtGlobal, EventIdIdentifier.UncaughtGlobal.ToString()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/Api/ApiValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace KesselRunFramework.Core.Infrastructure.Errors.Api 6 | { 7 | public class ApiValidationException : Exception 8 | { 9 | private readonly IList _errorMessages; 10 | 11 | public ApiValidationException(string message, IList validationErrors = null) 12 | : base(message) 13 | { 14 | _errorMessages = validationErrors ?? new List(); 15 | 16 | } 17 | 18 | public ApiValidationException(string message, Exception innerException, IList validationErrors = null) 19 | : base(message, innerException) 20 | { 21 | _errorMessages = validationErrors ?? new List(); 22 | } 23 | 24 | public IReadOnlyCollection Errors => new ReadOnlyCollection(_errorMessages); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/JsonOptionsConfigurer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 6 | { 7 | public class JsonOptionsConfigurer 8 | { 9 | public static void ConfigureJsonOptions(JsonOptions jsonOptions) 10 | { 11 | jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); 12 | jsonOptions.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; 13 | jsonOptions.JsonSerializerOptions.PropertyNameCaseInsensitive = true; 14 | jsonOptions.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; 15 | 16 | // Use the same serialization settings globally 🌐 17 | Common.JsonSerializerOptions = jsonOptions.JsonSerializerOptions; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/SwaggerFilters/ReplaceVersionWithExactValueInPath.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | using Swashbuckle.AspNetCore.SwaggerGen; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config.SwaggerFilters 5 | { 6 | public class ReplaceVersionWithExactValueInPath : IDocumentFilter 7 | { 8 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 9 | { 10 | var newPaths = new OpenApiPaths(); 11 | 12 | foreach (var swaggerDocPath in swaggerDoc.Paths) 13 | { 14 | // swaggerDoc.Info.Version gets set by c.SwaggerDoc in AddSwaggerGen 15 | newPaths.Add( 16 | swaggerDocPath.Key.Replace("v{version}", string.Concat("V", swaggerDoc.Info.Version)), 17 | swaggerDocPath.Value 18 | ); 19 | } 20 | 21 | swaggerDoc.Paths = newPaths; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Extensions/ControllerExtensions.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Response; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Extensions 6 | { 7 | public static class ControllerExtensions 8 | { 9 | public static IActionResult Forbidden(this ControllerBase source, ApiResponse apiResponse) 10 | { 11 | return source.StatusCode(StatusCodes.Status403Forbidden, apiResponse); 12 | } 13 | 14 | public static IActionResult ServerError(this ControllerBase source, string errors) 15 | { 16 | return source.StatusCode(StatusCodes.Status500InternalServerError, errors); 17 | } 18 | 19 | public static IActionResult ServerError(this ControllerBase source, ApiResponse apiResponse) 20 | { 21 | return source.StatusCode(StatusCodes.Status500InternalServerError, apiResponse); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ModelBinders/Providers/DateTimeModelBinderProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.ModelBinders.Providers 7 | { 8 | public class DateTimeModelBinderProvider : IModelBinderProvider 9 | { 10 | 11 | public IModelBinder GetBinder(ModelBinderProviderContext context) 12 | { 13 | if (context == null) 14 | throw new ArgumentNullException(nameof(context)); 15 | 16 | var modelType = context.Metadata.UnderlyingOrModelType; 17 | var loggerFactory = context.Services.GetRequiredService(); 18 | 19 | if (modelType == typeof(DateTime)) 20 | { 21 | return new UtcAwareDateTimeModelBinder(loggerFactory); 22 | } 23 | 24 | return null; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /KesselRun.Web.Api/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ActionFilters/SerilogMvcLoggingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Serilog; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.ActionFilters 7 | { 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class SerilogMvcLoggingAttribute : ActionFilterAttribute 10 | { 11 | public override void OnActionExecuting(ActionExecutingContext context) 12 | { 13 | var diagnosticContext = context.HttpContext.RequestServices.GetService(); 14 | 15 | diagnosticContext.Set("RouteData", context.ActionDescriptor.RouteValues); 16 | diagnosticContext.Set("ActionName", context.ActionDescriptor.DisplayName); 17 | diagnosticContext.Set("ActionId", context.ActionDescriptor.Id); 18 | diagnosticContext.Set("ValidationState", context.ModelState.IsValid); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | 6 | namespace KesselRunFramework.Core.Infrastructure.Extensions 7 | { 8 | public static class StreamExtensions 9 | { 10 | //public static async Task SerializeToJsonAndWrite(this Stream stream, T package) 11 | //{ 12 | // if (stream == null) throw new ArgumentNullException(nameof(stream)); 13 | // if (package == null) throw new ArgumentNullException(nameof(package)); 14 | 15 | // if (!stream.CanRead) 16 | // throw new NotSupportedException("It is not possible to read from this stream"); 17 | 18 | // using (var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true)) 19 | // { 20 | 21 | // JsonSerializer.Serialize(stream, package); 22 | // jsonTextWriter.Flush(); 23 | // } 24 | //} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Extensions 4 | { 5 | public static class StringExtensions 6 | { 7 | public static string FormatAs(this string source, params object[] paramObjects) 8 | { 9 | if (ReferenceEquals(source, null)) throw new ArgumentNullException(nameof(source)); 10 | if (ReferenceEquals(paramObjects, null)) throw new ArgumentNullException(nameof(paramObjects)); 11 | 12 | return string.Format(source, paramObjects); 13 | } 14 | 15 | public static T ParseEnum(this string source, bool? ignoreCase = null) 16 | where T : struct 17 | { 18 | if (ReferenceEquals(source, null)) throw new ArgumentNullException(nameof(source)); 19 | 20 | return ignoreCase.HasValue 21 | ? (T)Enum.Parse(typeof(T), source, ignoreCase.Value) 22 | : (T)Enum.Parse(typeof(T), source); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/AspNet.cs: -------------------------------------------------------------------------------- 1 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 2 | { 3 | public sealed class AspNet 4 | { 5 | public sealed class Request 6 | { 7 | public const string EndpointName = "EndpointName"; 8 | public const string FormParams = "FormParams"; 9 | public const string Protocol = "Protocol"; 10 | public const string QueryString = "QueryString"; 11 | public const string Scheme = "Scheme"; 12 | } 13 | 14 | public sealed class Mvc 15 | { 16 | public const string Action = "action"; 17 | public const string ActionTemplate = "[action]"; 18 | public const string Controller = "controller"; 19 | public const string ControllerTemplate = "[controller]"; 20 | public const string DefaultControllerTemplate = "v{version:apiVersion}/" + ControllerTemplate; 21 | public const string IdAction = ActionTemplate + "/{id:int}"; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Infrastructure/Diagnostic/Tracing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using KesselRun.Business.DataTransferObjects; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace KesselRun.Web.Api.Infrastructure.Diagnostic 6 | { 7 | public static class Tracing 8 | { 9 | private static readonly Action RegisteringUserTrace; 10 | 11 | const string PipelineHandler = "Pipeline Handler: "; 12 | 13 | static Tracing() 14 | { 15 | RegisteringUserTrace = LoggerMessage.Define( 16 | LogLevel.Debug, 17 | new EventId((int)WebTraceEventIdentifiers.RegisteringUserTrace, nameof(TraceRegisteringUser)), 18 | PipelineHandler + "{@registerUserPayload}" 19 | ); 20 | } 21 | 22 | public static void TraceRegisteringUser(this ILogger logger, RegisterUserPayloadDto registerUserPayload) 23 | { 24 | RegisteringUserTrace(logger, registerUserPayload, null); 25 | } 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Validation/SiteFluentValidatorFactory.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using System; 3 | 4 | namespace KesselRunFramework.AspNet.Validation 5 | { 6 | public class SiteFluentValidatorFactory : IValidatorFactory 7 | { 8 | private IServiceProvider Provider { get; set; } 9 | 10 | public SiteFluentValidatorFactory(IServiceProvider provider) 11 | { 12 | Provider = provider; 13 | } 14 | 15 | public virtual IValidator CreateInstance(Type validatorType) 16 | { 17 | return Provider.GetService(validatorType) as IValidator; 18 | } 19 | 20 | public IValidator GetValidator() 21 | { 22 | return Provider.GetService(typeof(IValidator)) as IValidator; 23 | } 24 | 25 | public IValidator GetValidator(Type type) 26 | { 27 | Type generic = typeof(IValidator<>); 28 | Type specific = generic.MakeGenericType(type); 29 | 30 | return Provider.GetService(specific) as IValidator; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/QueryHandlers/GetColorsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BusinessValidation; 2 | using KesselRun.Business.ApplicationServices; 3 | using KesselRun.Business.DataTransferObjects; 4 | using KesselRun.Web.Api.Messaging.Queries; 5 | using KesselRunFramework.Core.Infrastructure.Validation; 6 | using MediatR; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace KesselRun.Web.Api.Messaging.QueryHandlers 12 | { 13 | public class GetColorsQueryHandler : IRequestHandler, Validator>> 14 | { 15 | private readonly IColorsService _colorsService; 16 | 17 | public GetColorsQueryHandler(IColorsService colorsService) 18 | { 19 | _colorsService = colorsService; 20 | } 21 | 22 | public async Task, Validator>> Handle(GetColorsQuery request, CancellationToken cancellationToken) 23 | { 24 | return await _colorsService.GetColorsAsync(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Messaging/ValidateableResponse.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.Core.Infrastructure.Extensions; 2 | using System.Collections.Generic; 3 | 4 | namespace KesselRunFramework.Core.Infrastructure.Messaging 5 | { 6 | public class ValidateableResponse 7 | { 8 | public ValidateableResponse(IReadOnlyDictionary> validationErrors = null) 9 | { 10 | Errors = validationErrors ?? new Dictionary>(); 11 | } 12 | 13 | public bool IsValidResponse => Errors.None(); 14 | 15 | public IReadOnlyDictionary> Errors { get; } 16 | } 17 | 18 | public class ValidateableResponse : ValidateableResponse 19 | where TModel : class 20 | { 21 | 22 | public ValidateableResponse(TModel model, IReadOnlyDictionary> validationErrors = null) 23 | : base(validationErrors) 24 | { 25 | Result = model; 26 | } 27 | 28 | public TModel Result { get; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/QueryHandlers/GetWeatherQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using KesselRun.Business.DataTransferObjects.Weather; 4 | using KesselRun.Web.Api.HttpClients; 5 | using KesselRun.Web.Api.Messaging.Queries; 6 | using KesselRunFramework.Core.Infrastructure.Http; 7 | using MediatR; 8 | 9 | namespace KesselRun.Web.Api.Messaging.QueryHandlers 10 | { 11 | public class GetWeatherQueryHandler : IRequestHandler 12 | { 13 | private readonly WeatherClient _weatherClient; 14 | 15 | public GetWeatherQueryHandler(ITypedClientResolver typedClientResolver) 16 | { 17 | _weatherClient = typedClientResolver.GetTypedClient(); 18 | } 19 | 20 | public async Task Handle(GetWeatherQuery request, CancellationToken cancellationToken) 21 | { 22 | _weatherClient.City = request.City; 23 | _weatherClient.Units = request.Units; 24 | 25 | return await _weatherClient.GetWeather(cancellationToken); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Ioc/AppFrameworkRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using FluentValidation; 5 | using InControl.Framework.AspNet.Validation; 6 | using SimpleInjector; 7 | 8 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Ioc 9 | { 10 | public static class AppFrameworkRegistry 11 | { 12 | public static void RegisterAspNetCoreAbstractions(this Container container) 13 | { 14 | 15 | } 16 | 17 | public static void RegisterValidationAbstractions(this Container container, IEnumerable validationAssemblies) 18 | { 19 | if (container == null) throw new ArgumentNullException(nameof(container)); 20 | if (validationAssemblies == null) throw new ArgumentNullException(nameof(validationAssemblies)); 21 | 22 | container.Collection.Register(typeof(IValidator<>), validationAssemblies, Lifestyle.Singleton); 23 | 24 | container.Register(typeof(IValidator<>), typeof(CompositeValidator<>), Lifestyle.Singleton); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DavidRogersDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Messaging/Pipelines/OperationProfilingPipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using KesselRunFramework.AspNet.Infrastructure.Logging; 5 | using MediatR; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace KesselRunFramework.AspNet.Messaging.Pipelines 9 | { 10 | public class OperationProfilingPipeline : IPipelineBehavior 11 | where TRequest : IRequest 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public OperationProfilingPipeline(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 20 | { 21 | var stopwatch = Stopwatch.StartNew(); 22 | 23 | var result = await next(); 24 | 25 | stopwatch.Stop(); 26 | 27 | _logger.TraceMessageProfiling(stopwatch.ElapsedMilliseconds); 28 | 29 | return result; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/QueryHandlers/GetUserQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using KesselRun.Business.DataTransferObjects; 5 | using KesselRun.Web.Api.Messaging.Queries; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | 9 | namespace KesselRun.Web.Api.Messaging.QueryHandlers 10 | { 11 | public class GetUserQueryHandler : IRequestHandler 12 | { 13 | 14 | public GetUserQueryHandler() 15 | { 16 | 17 | } 18 | 19 | public async Task Handle(GetUserQuery request, CancellationToken cancellationToken) 20 | { 21 | // normally I'd hit a database and retrieve the user by id. 22 | // Here, if the Id of 10 is passed in, a User with UserName RonBurgandy will be returned. 23 | 24 | if(request.UserId.Equals(10)) 25 | return await Task.FromResult(new UserPayloadDto 26 | { 27 | UserName = "RonBurgandy" 28 | }); 29 | 30 | throw new Exception(""); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Mapping/MapperProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AutoMapper; 3 | using AutoMapper.Configuration; 4 | using SimpleInjector; 5 | 6 | namespace KesselRunFramework.Core.Infrastructure.Mapping 7 | { 8 | public class MapperProvider 9 | { 10 | private readonly Container _container; 11 | 12 | public MapperProvider(Container container) 13 | { 14 | _container = container; 15 | } 16 | 17 | public IMapper GetMapper(IEnumerable profiles) 18 | { 19 | var mapperConfigurationExpression = new MapperConfigurationExpression(); 20 | 21 | mapperConfigurationExpression.ConstructServicesUsing(_container.GetInstance); 22 | 23 | mapperConfigurationExpression.AddProfiles(profiles); 24 | 25 | var mapperConfiguration = new MapperConfiguration(mapperConfigurationExpression); 26 | 27 | //mc.AssertConfigurationIsValid(); 28 | 29 | IMapper mapper = new Mapper(mapperConfiguration, t => _container.GetInstance(t)); 30 | 31 | return mapper; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Middleware/OptionsDelegates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.SqlClient; 3 | using KesselRunFramework.AspNet.Response; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace KesselRunFramework.AspNet.Middleware 8 | { 9 | public static class OptionsDelegates 10 | { 11 | public static void UpdateApiErrorResponse(HttpContext context, Exception ex, OperationOutcome operationOutcome) 12 | { 13 | if (ex.GetType().Name.Equals(typeof(SqlException).Name, StringComparison.OrdinalIgnoreCase)) 14 | { 15 | operationOutcome.Message += "The exception was a database exception."; 16 | } 17 | } 18 | 19 | public static LogLevel DetermineLogLevel(Exception ex) 20 | { 21 | if (ex.Message.StartsWith("cannot open database", StringComparison.OrdinalIgnoreCase) || 22 | ex.Message.StartsWith("a network-related", StringComparison.OrdinalIgnoreCase)) 23 | { 24 | return LogLevel.Critical; 25 | } 26 | return LogLevel.Error; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/QueryHandlers/GetTvShowQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Web.Api.HttpClients; 2 | using KesselRun.Web.Api.Messaging.Queries; 3 | using MediatR; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using KesselRun.Business.DataTransferObjects.Media; 7 | using KesselRunFramework.Core.Infrastructure.Http; 8 | using KesselRunFramework.Core.Infrastructure.Validation; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace KesselRun.Web.Api.Messaging.QueryHandlers 12 | { 13 | public class GetTvShowQueryHandler : IRequestHandler> 14 | { 15 | private readonly OpenMovieDbClient _openMovieDbClient; 16 | 17 | public GetTvShowQueryHandler(ITypedClientResolver typedClientResolver) 18 | { 19 | _openMovieDbClient = typedClientResolver.GetTypedClient(); 20 | } 21 | 22 | public async Task> Handle(GetTvShowQuery request, CancellationToken cancellationToken) 23 | { 24 | return await _openMovieDbClient.GetShow(request.Title, request.Season, cancellationToken); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Extensions/ValidationResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Linq; 6 | 7 | namespace KesselRunFramework.Core.Infrastructure.Extensions 8 | { 9 | public static class ValidationResultExtensions 10 | { 11 | public static IReadOnlyDictionary> ToDictionary(this ValidationResult source, string prefix = null) 12 | { 13 | return new ReadOnlyDictionary>(source.Errors 14 | .ToLookup(e => e.PropertyName, e => e.ErrorMessage, StringComparer.Ordinal) 15 | .ToDictionary( 16 | lookupKey => string.IsNullOrEmpty(prefix) 17 | ? lookupKey.Key 18 | : string.IsNullOrEmpty(lookupKey.Key) 19 | ? prefix 20 | : prefix + "." + lookupKey.Key, 21 | lookupCollection => (IReadOnlyList)lookupCollection.ToList() 22 | )); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Application": { 3 | "GeneralConfig": { 4 | "TimeZoneCity": "Brisbane", 5 | "OpenApiInfoList": [ 6 | { 7 | "Contact": { 8 | "Name": "Some Contact", 9 | "Email": "support@kesselrun.com" 10 | }, 11 | "Description": "An API to service the XYZ application.", 12 | "TermsOfService": "https://www.vanlife.com.au", 13 | "Title": "Huge API", 14 | "Version": "1.0" 15 | }, 16 | { 17 | "Contact": { 18 | "Name": "Some new Contact", 19 | "Email": "admin@kesselrun.com" 20 | }, 21 | "Description": "An API to service the XYZ application.", 22 | "TermsOfService": "https://www.vanlife.com.au", 23 | "Title": "Huge API", 24 | "Version": "1.1" 25 | } 26 | ] 27 | }, 28 | "HstsSettings": { 29 | "MaxAge": 30 30 | }, 31 | "JwtSettings": { 32 | "CertificateThumbprint": "g81d70fa166e227efkki90ff14422455d7443gab3", 33 | "Issuers": [ "localhost:23000", "someidpurl.com" ], 34 | "ClockSkew": 300 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Extensions/EnvironmentLoggerConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using KesselRunFramework.AspNet.Infrastructure.Logging; 3 | using Serilog; 4 | using Serilog.Configuration; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.Extensions 7 | { 8 | public static class EnvironmentLoggerConfigurationExtensions 9 | { 10 | /// 11 | /// Enrich log events with a hash of the message template to uniquely identify the different event types. 12 | /// 13 | /// The enrichment configuration. 14 | /// Type: LoggerConfiguration. The LoggerConfiguration object resulting from the logger enrichment. 15 | /// enrichmentConfiguration 16 | public static LoggerConfiguration WithEventType(this LoggerEnrichmentConfiguration enrichmentConfiguration) 17 | { 18 | if (enrichmentConfiguration == null) throw new ArgumentNullException(nameof(enrichmentConfiguration)); 19 | return enrichmentConfiguration.With(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Errors/Api/ApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KesselRunFramework.Core.Infrastructure.Errors.Api 4 | { 5 | public class ApiException : Exception 6 | { 7 | public ApiException(string message) 8 | : base(message) 9 | { 10 | 11 | } 12 | 13 | public ApiException(string message, Exception innerException) 14 | : base(message, innerException) 15 | { 16 | 17 | } 18 | 19 | public ApiException(string message, ExceptionGravity exceptionGravity = ExceptionGravity.Error, int statusCode = 500) 20 | : base(message) 21 | { 22 | ExceptionGravity = exceptionGravity; 23 | StatusCode = statusCode; 24 | } 25 | 26 | public ApiException(string message, Exception innerException, ExceptionGravity exceptionGravity = ExceptionGravity.Error, int statusCode = 500) 27 | : base(message, innerException) 28 | { 29 | ExceptionGravity = exceptionGravity; 30 | StatusCode = statusCode; 31 | } 32 | 33 | public ExceptionGravity ExceptionGravity { get; set; } 34 | public int StatusCode { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Services/RequestStateService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Services 5 | { 6 | public class RequestStateService : IRequestStateService 7 | { 8 | private readonly IDictionary _requestItems; 9 | 10 | public RequestStateService(IHttpContextAccessor contextAccessor) 11 | { 12 | #if DEBUG 13 | _requestItems = contextAccessor?.HttpContext?.Items; 14 | #else 15 | _requestItems = contextAccessor.HttpContext.Items; 16 | #endif 17 | } 18 | 19 | public T GetItem(string key) 20 | { 21 | if (_requestItems.TryGetValue(key, out var item)) 22 | return (T) item; 23 | 24 | return default; 25 | } 26 | 27 | public bool HasItem(string key) 28 | { 29 | return _requestItems.ContainsKey(key); 30 | } 31 | 32 | public bool RemoveItem(string key) 33 | { 34 | return _requestItems.Remove(key); 35 | } 36 | 37 | public void SetItem(string key, T item) 38 | { 39 | _requestItems.Add(key, item); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Validation/NullValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using FluentValidation; 5 | using FluentValidation.Results; 6 | 7 | namespace KesselRunFramework.AspNet.Validation 8 | { 9 | public class NullValidator : IValidator 10 | { 11 | public ValidationResult Validate(T instance) => new ValidationResult(); 12 | public Task ValidateAsync(T instance, CancellationToken cancellation = new CancellationToken()) 13 | { 14 | throw new NotImplementedException(); 15 | } 16 | 17 | public ValidationResult Validate(IValidationContext context) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | 22 | public Task ValidateAsync(IValidationContext context, CancellationToken cancellation = new CancellationToken()) 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | 27 | public IValidatorDescriptor CreateDescriptor() 28 | { 29 | throw new NotImplementedException(); 30 | } 31 | 32 | public bool CanValidateInstancesOfType(Type type) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Infrastructure/AdoNetUtilities.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace KesselRunFramework.DataAccess.Infrastructure 8 | { 9 | public class AdoNetUtilities : IAdoNetUtilities 10 | { 11 | public string AddParametersToCommand(SqlCommand sqlCommand, IEnumerable ids, string paramPrefix = null) 12 | { 13 | if (ReferenceEquals(sqlCommand, null)) throw new ArgumentNullException(nameof(sqlCommand)); 14 | if (ReferenceEquals(ids, null)) throw new ArgumentNullException(nameof(ids)); 15 | 16 | var idsArray = ids as int[] ?? ids.ToArray(); // small perf measure 17 | 18 | if (idsArray.Length == 0) 19 | return string.Empty; 20 | 21 | var inClause = new StringBuilder(); 22 | 23 | var prefix = paramPrefix ?? "@p"; 24 | 25 | for (var i = 0; i < idsArray.Length; i++) 26 | { 27 | var paramName = string.Concat(prefix, i.ToString()); 28 | 29 | sqlCommand.Parameters.AddWithValue(paramName, idsArray[i]); 30 | 31 | inClause.Append(string.Concat(paramName, ",")); 32 | } 33 | 34 | return inClause.ToString().TrimEnd(','); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.0/UserController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Threading.Tasks; 3 | using KesselRun.Web.Api.Messaging.Queries; 4 | using KesselRunFramework.AspNet.Infrastructure; 5 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 6 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 7 | using KesselRunFramework.AspNet.Response; 8 | using MediatR; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | 12 | // ReSharper disable once CheckNamespace 13 | namespace KesselRun.Web.Api.Controllers.V1_0 14 | { 15 | [ApiVersion(Swagger.Versions.v1_0)] 16 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 17 | [Produces(MediaTypeNames.Application.Json)] 18 | 19 | public class UserController : AppApiMediatrController 20 | { 21 | public UserController(ICurrentUser currentUser, ILogger logger, IMediator mediator) 22 | : base(currentUser, logger, mediator) 23 | { 24 | } 25 | 26 | [HttpGet] 27 | [Route(AspNet.Mvc.IdAction)] 28 | [MapToApiVersion(Swagger.Versions.v1_0)] 29 | [ProducesResponseType(typeof(ApiResponse), 200)] 30 | public async Task GetUser(int id) 31 | { 32 | var user = await _mediator.Send(new GetUserQuery {UserId = id}); 33 | 34 | return OkResponse(user); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Common.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.Json; 3 | using KesselRunFramework.AspNet.Response; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding; 5 | using System.Collections.Generic; 6 | 7 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping 8 | { 9 | public class Common 10 | { 11 | public static JsonSerializerOptions JsonSerializerOptions { get; set; } 12 | 13 | public static ApiResponse ProcessInvalidModelState(ModelStateDictionary modelState) 14 | { 15 | var errors = GetErrorsFromModelState(modelState); 16 | 17 | var payload = new ApiResponse 18 | { 19 | Data = null, 20 | Outcome = OperationOutcome.ValidationFailOutcome(errors) 21 | }; 22 | 23 | return payload; 24 | } 25 | 26 | public static Dictionary> GetErrorsFromModelState(ModelStateDictionary modelState) 27 | { 28 | var errors = modelState 29 | .Where(m => m.Value.Errors.Any()) 30 | .ToDictionary( 31 | k => k.Key, 32 | v => v.Value.Errors.Select(e => e.ErrorMessage) 33 | ); 34 | 35 | return errors; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/HttpClients/TypedClientBase.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping; 2 | using System; 3 | using System.Collections.Specialized; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Net.Mime; 9 | using System.Text.Json; 10 | using System.Threading.Tasks; 11 | 12 | namespace KesselRun.Web.Api.HttpClients 13 | { 14 | public abstract class TypedClientBase 15 | { 16 | protected readonly HttpClient HttpClient; 17 | protected UriBuilder UriBuilder; 18 | protected NameValueCollection QueryStringParams; 19 | 20 | protected TypedClientBase(HttpClient httpClient) 21 | { 22 | HttpClient = httpClient; 23 | httpClient.Timeout = new TimeSpan(0, 0, 30); 24 | httpClient.DefaultRequestHeaders.Clear(); 25 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); 26 | httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(DecompressionMethods.GZip.ToString().ToLower())); 27 | } 28 | 29 | protected async ValueTask DeserializeAsync(Stream stream) 30 | { 31 | return await JsonSerializer.DeserializeAsync(stream, Common.JsonSerializerOptions); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/KesselRun.Web.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/CommandHandlers/RegisterNewUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using KesselRun.Web.Api.Messaging.Commands; 4 | using KesselRunFramework.AspNet.Response; 5 | using KesselRunFramework.Core.Infrastructure.Messaging; 6 | using MediatR; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace KesselRun.Web.Api.Messaging.CommandHandlers 10 | { 11 | public class RegisterNewUserCommandHandler : IRequestHandler>> 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public RegisterNewUserCommandHandler(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public async Task>> Handle(RegisterNewUserCommand request, CancellationToken cancellationToken) 21 | { 22 | // normally there would be code here which calls a service to persist the new user. 23 | // For demo purposes, I will just return the Id of 10 for the new imaginary user. 24 | 25 | return await Task.FromResult( 26 | new ValidateableResponse>( 27 | new ApiResponse 28 | { 29 | Data = 10, 30 | Outcome = OperationOutcome.SuccessfulOutcome 31 | }) 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Messaging/Validation/RegisterNewUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using FluentValidation; 4 | using KesselRun.Web.Api.Messaging.Commands; 5 | using Constants = KesselRun.Business.Invariants; 6 | 7 | namespace KesselRun.Web.Api.Messaging.Validation 8 | { 9 | public class RegisterNewUserCommandValidator : AbstractValidator 10 | { 11 | 12 | public RegisterNewUserCommandValidator() 13 | { 14 | RuleFor(u => u.Dto.UserName) 15 | .MustAsync(NotContainUserAlready) 16 | .WithMessage((u,userName) => $"{userName} {Constants.Validation.PartMessages.UserAlreadyExists}"); 17 | } 18 | 19 | async Task NotContainUserAlready(RegisterNewUserCommand dto, string userName, CancellationToken cancellation = new CancellationToken()) 20 | { 21 | // **************************************************************************************************** / 22 | // 23 | // Normally we would hit a database and retrieve the User here. 24 | // For demo purposes, I will enforce an assumption that a user with UserName "DaveRogers" already exists. 25 | // 26 | // **************************************************************************************************** / 27 | 28 | return await Task.FromResult(!userName.Equals("DaveRogers")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Extensions/ValidateableResponseExtensions.cs: -------------------------------------------------------------------------------- 1 | using KesselRunFramework.AspNet.Response; 2 | using KesselRunFramework.Core.Infrastructure.Messaging; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | 7 | namespace KesselRunFramework.AspNet.Infrastructure.Extensions 8 | { 9 | public static class ValidateableResponseExtensions 10 | { 11 | public static void AddToModelState(this ValidateableResponse source, ModelStateDictionary modelState) 12 | { 13 | foreach (var error in source.Errors) 14 | { 15 | foreach (var item in error.Value) 16 | { 17 | modelState.AddModelError(error.Key, item); 18 | } 19 | } 20 | } 21 | 22 | public static IActionResult ToUnprocessableRequestResult(this ValidateableResponse source, string message = null) 23 | { 24 | var outcome = OperationOutcome.ValidationFailOutcome(source.Errors, message ?? string.Empty); 25 | 26 | var apiResponse = new ApiResponse 27 | { 28 | Data = null, 29 | Outcome = outcome 30 | }; 31 | 32 | return new ObjectResult(apiResponse) 33 | { 34 | StatusCode = StatusCodes.Status422UnprocessableEntity 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Invariants/Swagger.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace KesselRunFramework.AspNet.Infrastructure.Invariants 4 | { 5 | [SuppressMessage("ReSharper", "InconsistentNaming")] 6 | public class Swagger 7 | { 8 | public sealed class EndPoint 9 | { 10 | public const string Name = "App API {0}"; 11 | public const string Url = "/swagger/{0}/swagger.json"; 12 | } 13 | 14 | public sealed class Info 15 | { 16 | public const string ContactEmail = "your.email@youbusiness.com"; 17 | public const string ContactName = "Your Name"; 18 | public const string Description = "A API to serve the App application."; 19 | public const string Title = "App API"; 20 | } 21 | public sealed class SecurityDefinition 22 | { 23 | public const string Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\""; 24 | public const string Name = "Authorization"; 25 | } 26 | 27 | public sealed class Versions 28 | { 29 | public const string v1_0 = "1.0"; 30 | public const string v1_1 = "1.1"; 31 | public const string VersionPrefix = "v"; 32 | } 33 | 34 | public sealed class DocVersions 35 | { 36 | public const string v1_0 = "v1.0"; 37 | public const string v1_1 = "v1.1"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/KesselRunFramework.AspNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Response/OperationOutcome.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace KesselRunFramework.AspNet.Response 4 | { 5 | public class OperationOutcome 6 | { 7 | public OperationOutcome() 8 | { 9 | Message = string.Empty; 10 | ErrorId = string.Empty; 11 | } 12 | 13 | public OpResult OpResult { get; set; } 14 | public string ErrorId { get; set; } 15 | public string Message { get; set; } 16 | public FailType FailType { get; set; } 17 | 18 | public dynamic Errors { get; set; } 19 | 20 | public static OperationOutcome SuccessfulOutcome => new OperationOutcome 21 | { 22 | Errors = Enumerable.Empty(), 23 | ErrorId = string.Empty, 24 | FailType = FailType.None, 25 | Message = string.Empty, 26 | OpResult = OpResult.Success 27 | }; 28 | 29 | public static OperationOutcome UnSuccessfulOutcome => new OperationOutcome 30 | { 31 | Errors = Enumerable.Empty(), 32 | ErrorId = string.Empty, 33 | FailType = FailType.Error, 34 | Message = string.Empty, 35 | OpResult = OpResult.Fail 36 | }; 37 | 38 | public static OperationOutcome ValidationFailOutcome(dynamic errors, string message = null) => new OperationOutcome 39 | { 40 | Errors = errors, 41 | ErrorId = string.Empty, 42 | FailType = FailType.ValidationFailure, 43 | Message = message ?? string.Empty, 44 | OpResult = OpResult.Fail 45 | }; 46 | } 47 | } -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/RequestLoggingConfigurer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Serilog; 3 | 4 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 5 | { 6 | public static class RequestLoggingConfigurer 7 | { 8 | public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext) 9 | { 10 | var request = httpContext.Request; 11 | 12 | // Set all the common properties available for every request 13 | diagnosticContext.Set(Microsoft.Net.Http.Headers.HeaderNames.Host, request.Host); 14 | diagnosticContext.Set(Invariants.AspNet.Request.Protocol, request.Protocol); 15 | diagnosticContext.Set(Invariants.AspNet.Request.Scheme, request.Scheme); 16 | 17 | // Only set it if available. You're not sending sensitive data in a querystring right?! 18 | if (request.QueryString.HasValue) 19 | { 20 | diagnosticContext.Set("QueryString", request.QueryString.Value); 21 | } 22 | 23 | // Set the content-type of the Response at this point 24 | diagnosticContext.Set(Microsoft.Net.Http.Headers.HeaderNames.ContentType, httpContext.Response.ContentType); 25 | 26 | // Retrieve the IEndpointFeature selected for the request 27 | var endpoint = httpContext.GetEndpoint(); 28 | 29 | if (!ReferenceEquals(endpoint, null)) 30 | { 31 | diagnosticContext.Set(Invariants.AspNet.Request.EndpointName, endpoint.DisplayName); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ActionFilters/ValidatorActionFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | 9 | namespace KesselRunFramework.AspNet.Infrastructure.ActionFilters 10 | { 11 | /// 12 | /// This attribute is for AJAX posts. No need to include the ModelState.IsValid check 13 | /// in the action method of the controller, as that check is performed here. 14 | /// 15 | public partial class ValidatorActionFilterAttribute : ActionFilterAttribute 16 | { 17 | public override async Task OnActionExecutionAsync( 18 | ActionExecutingContext context, 19 | ActionExecutionDelegate next) 20 | { 21 | if (context.ModelState.IsValid) 22 | { 23 | await next(); 24 | } 25 | else 26 | { 27 | 28 | var payload = Common.ProcessInvalidModelState(context.ModelState); 29 | 30 | var serializedModelState = JsonSerializer.Serialize(payload, Common.JsonSerializerOptions); 31 | 32 | var result = new ContentResult 33 | { 34 | Content = serializedModelState, 35 | ContentType = MediaTypeNames.Application.Json 36 | }; 37 | 38 | context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; 39 | context.Result = result; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.0/WeatherController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Threading.Tasks; 3 | using KesselRun.Business.DataTransferObjects; 4 | using KesselRun.Web.Api.Messaging.Commands; 5 | using KesselRun.Web.Api.Messaging.Queries; 6 | using KesselRunFramework.AspNet.Infrastructure; 7 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 8 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 9 | using KesselRunFramework.AspNet.Response; 10 | using MediatR; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace KesselRun.Web.Api.Controllers.V1._0 16 | { 17 | [ApiVersion(Swagger.Versions.v1_0)] 18 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 19 | [Produces(MediaTypeNames.Application.Json)] 20 | public class WeatherController : AppApiMediatrController 21 | { 22 | public WeatherController(ICurrentUser currentUser, ILogger logger, IMediator mediator) 23 | : base(currentUser, logger, mediator) 24 | { 25 | } 26 | 27 | [HttpGet] 28 | [Route(AspNet.Mvc.ActionTemplate)] 29 | [MapToApiVersion(Swagger.Versions.v1_0)] 30 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] 31 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] 32 | public async Task GetWeather([FromQuery]WeatherPayloadDto weatherPayloadDto) 33 | { 34 | var weather = await _mediator.Send(new GetWeatherQuery { City = weatherPayloadDto.City, Units = weatherPayloadDto.Units }); 35 | 36 | return Ok(weather); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Infrastructure/Extensions/DataReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | 4 | namespace KesselRunFramework.DataAccess.Infrastructure.Extensions 5 | { 6 | public static class DataReaderExtensions 7 | { 8 | public static string GetFirstString(this IDataReader source) 9 | { 10 | return source.GetString(0); 11 | } 12 | 13 | public static string GetSecondString(this IDataReader source) 14 | { 15 | return source.GetString(1); 16 | } 17 | 18 | public static byte GetFirstByte(this IDataReader source) 19 | { 20 | return source.GetByte(0); 21 | } 22 | 23 | public static byte GetSecondByte(this IDataReader source) 24 | { 25 | return source.GetByte(1); 26 | } 27 | 28 | public static int GetFirstInt(this IDataReader source) 29 | { 30 | return source.GetInt32(0); 31 | } 32 | 33 | public static int GetSecondInt(this IDataReader source) 34 | { 35 | return source.GetInt32(1); 36 | } 37 | 38 | public static short GetFirstShort(this IDataReader source) 39 | { 40 | return source.GetInt16(0); 41 | } 42 | 43 | public static short GetSecondShort(this IDataReader source) 44 | { 45 | return source.GetInt16(1); 46 | } 47 | 48 | public static DateTime GetFirstDate(this IDataReader source) 49 | { 50 | return source.GetDateTime(0); 51 | } 52 | 53 | public static DateTime GetSecondDate(this IDataReader source) 54 | { 55 | return source.GetDateTime(1); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/SwaggerFilters/SwaggerDefaultValuesFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.OpenApi.Models; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config.SwaggerFilters 7 | { 8 | /// 9 | /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. 10 | /// 11 | /// This is only required due to bugs in the . 12 | /// Once they are fixed and published, this class can be removed. 13 | public class SwaggerDefaultValuesFilter : IOperationFilter 14 | { 15 | /// 16 | /// Applies the filter to the specified operation using the given context. 17 | /// 18 | /// The operation to apply the filter to. 19 | /// The current operation filter context. 20 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 21 | { 22 | operation.Responses.Add("default", new OpenApiResponse 23 | { 24 | Description = "Problem response", 25 | Content = new Dictionary 26 | { 27 | ["application/json"] = new OpenApiMediaType 28 | { 29 | Schema = context.SchemaGenerator.GenerateSchema(typeof(ProblemDetails), context.SchemaRepository) 30 | } 31 | } 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.0/ColorsController.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Web.Api.Messaging.Queries; 2 | using KesselRunFramework.AspNet.Infrastructure; 3 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 4 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 5 | using KesselRunFramework.AspNet.Response; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | using System.Linq; 11 | using System.Net.Mime; 12 | using System.Threading.Tasks; 13 | 14 | namespace KesselRun.Web.Api.Controllers.V1._0 15 | { 16 | [ApiVersion(Swagger.Versions.v1_0)] 17 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 18 | [Produces(MediaTypeNames.Application.Json)] 19 | public class ColorsController : AppApiMediatrController 20 | { 21 | public ColorsController( 22 | ICurrentUser currentUser, 23 | ILogger logger, 24 | IMediator mediator) 25 | : base(currentUser, logger, mediator) 26 | { 27 | 28 | } 29 | 30 | [HttpGet] 31 | [Route(AspNet.Mvc.ActionTemplate)] 32 | [MapToApiVersion(Swagger.Versions.v1_0)] 33 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] 34 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)] 35 | public async Task GetColors() 36 | { 37 | var colors = await _mediator.Send(new GetColorsQuery()); 38 | 39 | return colors.Match( 40 | result => OkResponse(colors.LeftOrDefault()), 41 | error => UnprocessableEntityResponse(string.Empty, OperationOutcome.ValidationFailOutcome(colors.RightOrDefault().ValidationFailures)) 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/DbConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Data.SqlClient; 6 | 7 | namespace KesselRunFramework.DataAccess 8 | { 9 | public static class DbConnectionExtensions 10 | { 11 | public static IDataReader ExecuteCommandQuery(this SqlConnection source, string query, SqlParameter[] parameters = null) 12 | { 13 | if (ReferenceEquals(source, null)) throw new ArgumentNullException(nameof(source)); 14 | if (string.IsNullOrWhiteSpace(query)) throw new ArgumentException(nameof(query)); 15 | 16 | var command = new SqlCommand { Connection = source }; 17 | 18 | if (!ReferenceEquals(parameters, null)) 19 | command.Parameters.AddRange(parameters); 20 | 21 | command.CommandText = query; 22 | command.CommandType = CommandType.Text; 23 | 24 | return command.ExecuteReader(); 25 | } 26 | 27 | public static async Task ExecuteCommandQueryAsync(this SqlConnection source, string query, CancellationToken cancellationToken, SqlParameter[] parameters = null) 28 | { 29 | if (ReferenceEquals(source, null)) throw new ArgumentNullException(nameof(source)); 30 | if (string.IsNullOrWhiteSpace(query)) throw new ArgumentException(nameof(query)); 31 | 32 | var command = new SqlCommand { Connection = source }; 33 | 34 | if (!ReferenceEquals(parameters, null)) 35 | command.Parameters.AddRange(parameters); 36 | 37 | command.CommandText = query; 38 | command.CommandType = CommandType.Text; 39 | 40 | return await command.ExecuteReaderAsync(cancellationToken); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.0/MediaContentController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Threading.Tasks; 3 | using KesselRun.Business.DataTransferObjects; 4 | using KesselRun.Web.Api.Messaging.Queries; 5 | using KesselRunFramework.AspNet.Infrastructure; 6 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 7 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 8 | using KesselRunFramework.AspNet.Response; 9 | using MediatR; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace KesselRun.Web.Api.Controllers.V1._0 15 | { 16 | [ApiVersion(Swagger.Versions.v1_0)] 17 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 18 | [Produces(MediaTypeNames.Application.Json)] 19 | public class MediaContentController : AppApiMediatrController 20 | { 21 | public MediaContentController(ICurrentUser currentUser, ILogger logger, IMediator mediator) 22 | : base(currentUser, logger, mediator) 23 | { 24 | } 25 | 26 | [HttpGet] 27 | [Route(AspNet.Mvc.ActionTemplate)] 28 | [MapToApiVersion(Swagger.Versions.v1_0)] 29 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] 30 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] 31 | public async Task GetTvShow([FromQuery]TvShowPayloadDto tvShowPayload) 32 | { 33 | var tvShow = await _mediator.Send(new GetTvShowQuery { Season = tvShowPayload.Season, Title = tvShowPayload.Title }); 34 | 35 | return tvShow.Match( 36 | t => OkResponse(tvShow.LeftOrDefault()), 37 | p => InternalServerErrorResponse(tvShow.RightOrDefault(), OperationOutcome.UnSuccessfulOutcome) 38 | ); 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.1/ColorsController.cs: -------------------------------------------------------------------------------- 1 | using KesselRun.Web.Api.Messaging.Queries; 2 | using KesselRunFramework.AspNet.Infrastructure; 3 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 4 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 5 | using KesselRunFramework.AspNet.Response; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | using System.Linq; 11 | using System.Net.Mime; 12 | using System.Threading.Tasks; 13 | using KesselRun.Business.DataTransferObjects; 14 | 15 | namespace KesselRun.Web.Api.Controllers.V1._1 16 | { 17 | [ApiVersion(Swagger.Versions.v1_1)] 18 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 19 | [Produces(MediaTypeNames.Application.Json)] 20 | public class ColorsController : AppApiMediatrController 21 | { 22 | public ColorsController( 23 | ICurrentUser currentUser, 24 | ILogger logger, 25 | IMediator mediator) 26 | : base(currentUser, logger, mediator) 27 | { 28 | 29 | } 30 | 31 | [HttpGet] 32 | [Route(AspNet.Mvc.ActionTemplate)] 33 | [MapToApiVersion(Swagger.Versions.v1_1)] 34 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] 35 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)] 36 | public async Task GetColors() 37 | { 38 | var colors = await _mediator.Send(new GetColorsQuery()); 39 | 40 | return colors.Match( 41 | result => OkResponse(colors.LeftOrDefault()), 42 | error => UnprocessableEntityResponse(string.Empty, OperationOutcome.ValidationFailOutcome(colors.RightOrDefault().ValidationFailures)) 43 | ); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using SimpleInjector; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 7 | { 8 | public static class ApplicationBuilderExtensions 9 | { 10 | public static void ConfigureMiddlewareForEnvironments(this IApplicationBuilder app, 11 | IWebHostEnvironment env, 12 | Container container) 13 | { 14 | if (env.IsDevelopment()) 15 | { 16 | app.UseDeveloperExceptionPage(); 17 | container.Verify(); 18 | } 19 | else 20 | { 21 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 22 | app.UseHsts(); 23 | } 24 | } 25 | 26 | public static void UseSimpleInjectorForDomain(this IApplicationBuilder app, Container container) 27 | { 28 | // UseSimpleInjector() enables framework services to be injected into 29 | // application components, resolved by Simple Injector. 30 | app.UseSimpleInjector(container, options => 31 | { 32 | // Add custom Simple Injector-created middleware to the ASP.NET pipeline. 33 | //options.UseMiddleware(app); 34 | //options.UseMiddleware(app); 35 | 36 | // Optionally, allow application components to depend on the 37 | // non-generic Microsoft.Extensions.Logging.ILogger 38 | // or Microsoft.Extensions.Localization.IStringLocalizer abstractions. 39 | 40 | //options.UseLocalization(); 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/ServicesCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using KesselRunFramework.AspNet.Infrastructure.Extensions; 4 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Cors.Infrastructure; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | namespace InControl.Framework.AspNet.Infrastructure.Bootstrapping.Config 12 | { 13 | public static class ServicesCollectionExtensions 14 | { 15 | public static void AddCorsByEnvironment(this IServiceCollection services, 16 | IWebHostEnvironment webHostEnvironment, 17 | Action buildAction = null) 18 | { 19 | if (webHostEnvironment.IsDevelopmentOrStaging()) 20 | { 21 | services.AddCors(options => 22 | options.AddPolicy(Browser.CorsPolicy.AllowAll, 23 | p => p.AllowAnyHeader() 24 | .AllowAnyMethod() 25 | .AllowAnyOrigin() 26 | ) 27 | ); 28 | } 29 | } 30 | 31 | public static void AddRedirect(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, int maxAge, int port = 443) 32 | { 33 | var isProduction = webHostEnvironment.IsProduction(); 34 | 35 | if (isProduction) 36 | { 37 | // Only add Hsts for production. 38 | services.AddHsts(c => 39 | { 40 | c.Preload = false; 41 | c.IncludeSubDomains = false; 42 | c.MaxAge = TimeSpan.FromDays(maxAge); 43 | }); 44 | } 45 | 46 | services.AddHttpsRedirection(c => 47 | { 48 | c.HttpsPort = port; 49 | c.RedirectStatusCode = isProduction 50 | ? (int)HttpStatusCode.PermanentRedirect 51 | : (int)HttpStatusCode.TemporaryRedirect; 52 | }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Controllers/V1.0/RegisterUserController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Mime; 3 | using System.Threading.Tasks; 4 | using KesselRun.Business.DataTransferObjects; 5 | using KesselRun.Web.Api.Controllers.V1_0; 6 | using KesselRun.Web.Api.Messaging.Commands; 7 | using KesselRunFramework.AspNet.Infrastructure; 8 | using KesselRunFramework.AspNet.Infrastructure.Controllers; 9 | using KesselRunFramework.AspNet.Infrastructure.Extensions; 10 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 11 | using KesselRunFramework.AspNet.Response; 12 | using MediatR; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Mvc; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace KesselRun.Web.Api.Controllers.V1._0 18 | { 19 | [ApiVersion(Swagger.Versions.v1_0)] 20 | [Route(AspNet.Mvc.DefaultControllerTemplate)] 21 | [Produces(MediaTypeNames.Application.Json)] 22 | public class RegisterUserController : AppApiMediatrController 23 | { 24 | public RegisterUserController( 25 | ICurrentUser currentUser, 26 | ILogger logger, 27 | IMediator mediator) 28 | : base(currentUser, logger, mediator) 29 | { 30 | } 31 | 32 | [HttpPost] 33 | [Route(AspNet.Mvc.ActionTemplate)] 34 | [MapToApiVersion(Swagger.Versions.v1_0)] 35 | [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] 36 | [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status400BadRequest)] 37 | public async Task CreateUser([FromForm]RegisterUserPayloadDto dto) 38 | { 39 | var result = await _mediator.Send(new RegisterNewUserCommand 40 | { 41 | Dto = dto 42 | }); 43 | 44 | return result.IsValidResponse 45 | ? CreatedAtRoute(routeValues: new 46 | { 47 | controller = "User", 48 | action = nameof(UserController.GetUser), 49 | id = result.Result.Data, 50 | version = HttpContext.GetRequestedApiVersion().ToString() 51 | }, result.Result) 52 | : result.ToUnprocessableRequestResult(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Validation/Either.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * This class was taken from this GitHub repo https://github.com/mikhailshilkov/mikhailio-samples 3 | * under an MIT licence. 4 | */ 5 | 6 | using System; 7 | 8 | namespace KesselRunFramework.Core.Infrastructure.Validation 9 | { 10 | 11 | /// 12 | /// Functional data data to represent a discriminated 13 | /// union of two possible types. 14 | /// 15 | /// Type of "Left" item. 16 | /// Type of "Right" item. 17 | public class Either 18 | { 19 | private readonly TL _left; 20 | private readonly TR _right; 21 | private readonly bool _isLeft; 22 | 23 | public Either(TL left) 24 | { 25 | _left = left; 26 | _isLeft = true; 27 | } 28 | 29 | public Either(TR right) 30 | { 31 | _right = right; 32 | _isLeft = false; 33 | } 34 | 35 | public T Match(Func leftFunc, Func rightFunc) 36 | { 37 | if (ReferenceEquals(leftFunc, null)) 38 | throw new ArgumentNullException(nameof(leftFunc)); 39 | 40 | if (ReferenceEquals(rightFunc, null)) 41 | throw new ArgumentNullException(nameof(rightFunc)); 42 | 43 | return _isLeft ? leftFunc(_left) : rightFunc(_right); 44 | } 45 | 46 | /// 47 | /// If right value is assigned, execute an action on it. 48 | /// 49 | /// Action to execute. 50 | public void DoRight(Action rightAction) 51 | { 52 | if (ReferenceEquals(rightAction, null)) 53 | throw new ArgumentNullException(nameof(rightAction)); 54 | 55 | if (!_isLeft) 56 | rightAction(_right); 57 | } 58 | 59 | public TL LeftOrDefault() => Match(l => l, r => default(TL)); 60 | 61 | public TR RightOrDefault() => Match(l => default(TR), r => r); 62 | 63 | public static implicit operator Either(TL left) => new Either(left); 64 | 65 | public static implicit operator Either(TR right) => new Either(right); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using KesselRunFramework.AspNet.Infrastructure.Extensions; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Hosting; 7 | using Serilog; 8 | using Serilog.Events; 9 | using Serilog.Formatting.Json; 10 | 11 | namespace KesselRun.Web.Api 12 | { 13 | public class Program 14 | { 15 | internal static string BasePath = Directory.GetCurrentDirectory(); 16 | 17 | public static void Main(string[] args) 18 | { 19 | var configuration = new ConfigurationBuilder() 20 | .SetBasePath(BasePath) 21 | .AddJsonFile("serilogsettings.json") 22 | .Build(); 23 | 24 | Log.Logger = new LoggerConfiguration() 25 | .ReadFrom.Configuration(configuration) 26 | .CreateLogger(); 27 | //Log.Logger = new LoggerConfiguration() 28 | // .Enrich.FromLogContext() 29 | // .Enrich.WithEventType() 30 | // .MinimumLevel.Debug() 31 | // .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) 32 | // .WriteTo.Console() 33 | // .WriteTo.File( 34 | // new JsonFormatter(), 35 | // "apilog.log", 36 | // rollingInterval: RollingInterval.Day, 37 | // restrictedToMinimumLevel: LogEventLevel.Verbose 38 | // ) 39 | // .CreateLogger(); 40 | 41 | try 42 | { 43 | Log.Information("Booting up"); 44 | CreateHostBuilder(args).Build().Run(); 45 | } 46 | catch (Exception exception) 47 | { 48 | Log.Fatal(exception, "Application start-up failed"); 49 | } 50 | finally 51 | { 52 | Log.CloseAndFlush(); 53 | } 54 | } 55 | 56 | public static IHostBuilder CreateHostBuilder(string[] args) => 57 | Host.CreateDefaultBuilder(args) 58 | .UseSerilog() 59 | .ConfigureWebHostDefaults(webBuilder => 60 | { 61 | webBuilder.UseStartup(); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /KesselRun.Business/ApplicationServices/ColorsService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using BusinessValidation; 3 | using KesselRun.Business.DataTransferObjects; 4 | using KesselRunFramework.Core; 5 | using KesselRunFramework.Core.Infrastructure.Validation; 6 | using System.Collections.Generic; 7 | using System.Drawing; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace KesselRun.Business.ApplicationServices 12 | { 13 | public class ColorsService : ApplicationService, IColorsService 14 | { 15 | public ColorsService(IMapper mapper) 16 | : base(mapper) 17 | { 18 | } 19 | 20 | public async Task, Validator>> GetColorsAsync() 21 | { 22 | // This example is a bit silly. 23 | // Here, for the purposes of this example, the business rule is that the collection of colors that 24 | // is returned must not be empty. 25 | // To exercise the code, comment out option 1 if you want validation to pass and option 2 if you want validation to fail. 26 | 27 | /****************************** OPTION 1 ******************************/ 28 | var colorPayloadDtos = new List 29 | { 30 | new ColorPayloadDto{ Id = Color.Gainsboro.ToArgb(), IsKnownColor = Color.Gainsboro.IsKnownColor, Color = Color.Gainsboro.Name}, 31 | new ColorPayloadDto{ Id = Color.SaddleBrown.ToArgb(), IsKnownColor = Color.SaddleBrown.IsKnownColor ,Color =Color.SaddleBrown.Name}, 32 | new ColorPayloadDto{ Id = Color.PaleGreen.ToArgb(), IsKnownColor = Color.PaleGreen.IsKnownColor,Color= Color.PaleGreen.Name}, 33 | new ColorPayloadDto{ Id = Color.DarkBlue.ToArgb(), IsKnownColor = Color.DarkBlue.IsKnownColor,Color= Color.DarkBlue.Name} 34 | }; 35 | 36 | /****************************** OPTION 2 ******************************/ 37 | //var colorPayloadDtos = new List(0); 38 | 39 | var validator = new Validator(); 40 | validator.Validate("NoColors", "The color collection must contain at least one color.", colorPayloadDtos, c => c.Any()); 41 | 42 | if (validator) 43 | return await Task.FromResult(colorPayloadDtos); 44 | 45 | return await Task.FromResult(validator); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /KesselRunFramework.Core/Infrastructure/Mapping/ProfileBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using AutoMapper; 6 | 7 | namespace KesselRunFramework.Core.Infrastructure.Mapping 8 | { 9 | public class ProfileBase : Profile 10 | { 11 | protected ProfileBase(string profileName) 12 | : base(profileName) 13 | { 14 | ConfigureProfile(); 15 | } 16 | 17 | protected void ConfigureProfile() 18 | { 19 | SourceMemberNamingConvention = new PascalCaseNamingConvention(); 20 | DestinationMemberNamingConvention = new PascalCaseNamingConvention(); 21 | 22 | ShouldMapField = field => false; 23 | } 24 | 25 | public void InitializeMappings(IEnumerable assemblies) 26 | { 27 | var assemblyTypes = assemblies.SelectMany(a => a.GetExportedTypes()); 28 | 29 | LoadStandardMappings(assemblyTypes); 30 | LoadCustomMappings(assemblyTypes); 31 | } 32 | 33 | private void LoadStandardMappings(IEnumerable coreAssemblyTypes) 34 | { 35 | var maps = (from t in coreAssemblyTypes 36 | from i in t.GetInterfaces() 37 | where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && 38 | !t.IsAbstract && 39 | !t.IsInterface 40 | select new 41 | { 42 | Source = i.GetGenericArguments()[0], 43 | Destination = t 44 | }).ToArray(); 45 | 46 | foreach (var map in maps) 47 | { 48 | CreateMap(map.Source, map.Destination); 49 | } 50 | } 51 | 52 | private void LoadCustomMappings(IEnumerable coreAssemblyTypes) 53 | { 54 | var maps = (from t in coreAssemblyTypes 55 | from i in t.GetInterfaces() 56 | where typeof(ICustomMapSomeClasses).IsAssignableFrom(t) && 57 | !t.IsAbstract && 58 | !t.IsInterface 59 | select (ICustomMapSomeClasses)Activator.CreateInstance(t)).ToArray(); 60 | 61 | foreach (var map in maps) 62 | { 63 | map.CreateMappings(this); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/HttpClientConfigurer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Polly; 7 | using Polly.Extensions.Http; 8 | 9 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 10 | { 11 | public static class HttpClientConfigurer 12 | { 13 | public static void RegisterTypedHttpClients(this IServiceCollection services, IEnumerable types) 14 | { 15 | if (services == null) throw new ArgumentNullException(nameof(services)); 16 | if (types == null) throw new ArgumentNullException(nameof(types)); 17 | 18 | var typesArray = types as Type[] ?? types.ToArray(); 19 | 20 | if (typesArray.Any()) 21 | { 22 | var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy(); 23 | 24 | var retryPolicy = HttpPolicyExtensions 25 | .HandleTransientHttpError() 26 | .WaitAndRetryAsync(new[] 27 | { 28 | TimeSpan.FromSeconds(2), 29 | TimeSpan.FromSeconds(4), 30 | TimeSpan.FromSeconds(8) 31 | }); 32 | 33 | var addHttpClientMethod = typeof(HttpClientFactoryServiceCollectionExtensions).GetMethods() 34 | .Single( 35 | m => 36 | m.Name == "AddHttpClient" && 37 | m.GetGenericArguments().Length == 1 && 38 | m.GetParameters().Length == 1 && 39 | m.GetParameters()[0].ParameterType == typeof(IServiceCollection) 40 | ); 41 | 42 | for (int i = 0; i < typesArray.Length; i++) 43 | { 44 | var genericMethod = addHttpClientMethod.MakeGenericMethod(typesArray[i]); 45 | 46 | var httpClientBuilder = (IHttpClientBuilder)genericMethod.Invoke(null, new[] { services }); 47 | 48 | httpClientBuilder.ConfigurePrimaryHttpMessageHandler(handler => new HttpClientHandler 49 | { 50 | AutomaticDecompression = System.Net.DecompressionMethods.GZip 51 | }).AddPolicyHandler( 52 | request => request.Method == HttpMethod.Get 53 | ? retryPolicy 54 | : noOpPolicy 55 | ); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Ioc/JimmyBogardRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using AutoMapper; 5 | using KesselRunFramework.Core.Infrastructure.Mapping; 6 | using MediatR; 7 | using SimpleInjector; 8 | 9 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Ioc 10 | { 11 | public static class JimmyBogardRegistry 12 | { 13 | public static void RegisterMediatRAbstractions(this Container container, Assembly[] mediatrAssemblies, Type[] pipelineBehaviors) 14 | { 15 | if (container == null) throw new ArgumentNullException(nameof(container)); 16 | if (mediatrAssemblies == null) throw new ArgumentNullException(nameof(mediatrAssemblies)); 17 | if (pipelineBehaviors == null) throw new ArgumentNullException(nameof(pipelineBehaviors)); 18 | 19 | container.RegisterSingleton(); 20 | container.Register(typeof(IRequestHandler<,>), mediatrAssemblies, Lifestyle.Scoped); 21 | 22 | // we have to do this because by default, generic type definitions (such as the Constrained Notification Handler) won't be registered 23 | var notificationHandlerTypes = container.GetTypesToRegister(typeof(INotificationHandler<>), mediatrAssemblies, new TypesToRegisterOptions 24 | { 25 | IncludeGenericTypeDefinitions = true, 26 | IncludeComposites = false, 27 | }); 28 | 29 | container.Collection.Register(typeof(INotificationHandler<>), notificationHandlerTypes); 30 | 31 | // Register Pipelines here 32 | container.Collection.Register(typeof(IPipelineBehavior<,>), pipelineBehaviors); 33 | 34 | container.Register(() => new ServiceFactory(container.GetInstance), Lifestyle.Singleton); 35 | 36 | } 37 | 38 | public static void RegisterAutomapperAbstractions(this Container container, Profile[] automapperProfiles) 39 | { 40 | if (container == null) throw new ArgumentNullException(nameof(container)); 41 | if (automapperProfiles == null) throw new ArgumentNullException(nameof(automapperProfiles)); 42 | 43 | container.Register(Lifestyle.Singleton); 44 | container.RegisterSingleton(() => GetMapper(container, automapperProfiles)); 45 | } 46 | 47 | private static IMapper GetMapper(Container container, IEnumerable profiles) 48 | { 49 | var mapperProvider = container.GetInstance(); 50 | return mapperProvider.GetMapper(profiles); 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Ioc/ApplicationServicesRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using KesselRunFramework.Core; 5 | using Microsoft.Extensions.Configuration; 6 | using SimpleInjector; 7 | 8 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Ioc 9 | { 10 | public static class ApplicationServicesRegistry 11 | { 12 | public static void RegisterApplicationServices(this Container container, Assembly assembly, IConfiguration configuration, string applicationServicesFullNs) 13 | { 14 | if (container == null) throw new ArgumentNullException(nameof(container)); 15 | if (assembly == null) throw new ArgumentNullException(nameof(assembly)); 16 | if (configuration == null) throw new ArgumentNullException(nameof(configuration)); 17 | if (applicationServicesFullNs == null) throw new ArgumentNullException(nameof(applicationServicesFullNs)); 18 | if (applicationServicesFullNs.Trim().Equals(string.Empty)) throw new ArgumentException(nameof(applicationServicesFullNs) + " cannot be an empty string."); 19 | 20 | var applicationServicesInterface = typeof(IApplicationService); 21 | var applicationServicesInterfaceName = nameof(IApplicationService); 22 | var applicationDataServicesInterface = typeof(IApplicationDataService); 23 | var applicationDataServicesInterfaceName = nameof(IApplicationDataService); 24 | 25 | var applicationServices = assembly.GetExportedTypes() 26 | .Where(t => applicationDataServicesInterface.IsAssignableFrom(t) 27 | || applicationServicesInterface.IsAssignableFrom(t) 28 | ) 29 | .Where(t => !t.IsInterface) 30 | .Select(t => new 31 | { 32 | Type = t, 33 | Interfaces = t.GetInterfaces() 34 | }) 35 | .Where(t => t.Interfaces.Any()) 36 | .Select(type => new 37 | { 38 | Service = type.Interfaces.First( 39 | i => !i.Name.Equals(applicationServicesInterfaceName, StringComparison.Ordinal) 40 | && !i.Name.Equals(applicationDataServicesInterfaceName, StringComparison.Ordinal) 41 | ), 42 | Implementation = type.Type 43 | }); 44 | 45 | foreach (var applicationService in applicationServices) 46 | { 47 | container.Register(applicationService.Service, applicationService.Implementation, Lifestyle.Singleton); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Services/SessionStateService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace KesselRunFramework.AspNet.Infrastructure.Services 7 | { 8 | public class SessionStateService : ISessionStateService 9 | { 10 | private readonly ISession _sessionState; 11 | 12 | public SessionStateService(IHttpContextAccessor contextAccessor) 13 | { 14 | #if DEBUG 15 | _sessionState = contextAccessor?.HttpContext?.Session; // is null when container.Verify() is called. Only called in Development. 16 | #else 17 | _sessionState = contextAccessor.HttpContext.Session; 18 | #endif 19 | } 20 | 21 | public bool? GetBoolean(string key) 22 | { 23 | var data = _sessionState.Get(key); 24 | 25 | if (ReferenceEquals(data, null)) return null; 26 | 27 | return BitConverter.ToBoolean(data, 0); 28 | } 29 | 30 | public DateTime? GetDateTime(string key) 31 | { 32 | var data = _sessionState.Get(key); 33 | 34 | if (ReferenceEquals(data, null)) return null; 35 | 36 | long ticks = BitConverter.ToInt64(data, 0); 37 | 38 | return new DateTime(ticks); 39 | } 40 | 41 | public DateTimeOffset? GetDateTimeOffset(string key) 42 | { 43 | var data = _sessionState.GetString(key); 44 | 45 | if (ReferenceEquals(data, null)) return null; 46 | 47 | return JsonSerializer.Deserialize(data); 48 | } 49 | 50 | public T GetObject(string key) 51 | { 52 | var value = _sessionState.GetString(key); 53 | 54 | return value == null 55 | ? default(T) 56 | : JsonSerializer.Deserialize(value); 57 | } 58 | 59 | public int? GetInt(string key) 60 | { 61 | return _sessionState.GetInt32(key); 62 | } 63 | 64 | public bool HasItem(string key) 65 | { 66 | return _sessionState.Keys.Contains(key); 67 | } 68 | 69 | public void SetBoolean(string key, bool value) 70 | { 71 | _sessionState.Set(key, BitConverter.GetBytes(value)); 72 | } 73 | 74 | public void SetDateTime(string key, DateTime value) 75 | { 76 | long ticks = value.Ticks; 77 | var data = BitConverter.GetBytes(ticks); 78 | 79 | _sessionState.Set(key, data); 80 | } 81 | 82 | public void SetDateTimeOffset(string key, DateTimeOffset value) 83 | { 84 | _sessionState.SetString(key, JsonSerializer.Serialize(value)); 85 | } 86 | 87 | public void SetObject(string key, T item) 88 | { 89 | _sessionState.SetString(key, JsonSerializer.Serialize(item)); 90 | } 91 | 92 | public void SetInt(string key, int value) 93 | { 94 | _sessionState.SetInt32(key, value); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/ConfigurationAddExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using KesselRunFramework.AspNet.Infrastructure.ActionFilters; 3 | using KesselRunFramework.AspNet.Infrastructure.Identity; 4 | using KesselRunFramework.AspNet.Infrastructure.Services; 5 | using KesselRunFramework.DataAccess; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc.Infrastructure; 9 | using Microsoft.Extensions.Caching.Distributed; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using SimpleInjector; 13 | using SimpleInjector.Lifestyles; 14 | 15 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 16 | { 17 | public static class ConfigurationAddExtensions 18 | { 19 | public static void ConfigureAppServices(this IServiceCollection services, IWebHostEnvironment env, Container container) 20 | { 21 | if (services == null) throw new ArgumentNullException(nameof(services)); 22 | if (env == null) throw new ArgumentNullException(nameof(env)); 23 | 24 | // For ASP.NET centric stuff, regester it with the framework container i.e. IServiceCollection 25 | services.AddHttpContextAccessor(); 26 | services.AddDistributedMemoryCache(); 27 | services.AddSession(); 28 | services.AddSingleton(); 29 | services.AddSingleton(); 30 | 31 | services.AddScoped(); 32 | services.AddScoped(); 33 | services.AddScoped(s => container.GetInstance()); 34 | services.AddScoped(); 35 | services.AddScoped(); 36 | 37 | services.AddSimpleInjector(container, options => 38 | { 39 | // AddAspNetCore() method wraps web requests in a Simple Injector scope. 40 | options.AddAspNetCore() 41 | // Ensure activation of a specific framework type to be created by 42 | // Simple Injector instead of the built-in configuration system. 43 | .AddControllerActivation(); 44 | 45 | services.AddScoped(c => container.GetInstance()); 46 | 47 | options.AddLogging(); // <-- This registers all logging abstractions 48 | 49 | 50 | // Important for things like Http clients which are owned by the ServicesCollection but get 51 | // injected into things like MediatR request handlers, which use SimpleInjector for its IOC needs. 52 | options.AutoCrossWireFrameworkComponents = true; 53 | 54 | //.AddViewComponentActivation() 55 | //.AddPageModelActivation() 56 | //.AddTagHelperActivation(); 57 | }); 58 | 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Validation/CompositeValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentValidation; 7 | using ValidationResult = FluentValidation.Results.ValidationResult; 8 | 9 | namespace InControl.Framework.AspNet.Validation 10 | { 11 | public class CompositeValidator : IValidator 12 | { 13 | private readonly IEnumerable> _validators; 14 | 15 | public CompositeValidator(IEnumerable> validators) 16 | { 17 | _validators = validators; 18 | } 19 | 20 | public CascadeMode CascadeMode { get; set; } 21 | 22 | public ValidationResult Validate(IValidationContext context) 23 | { 24 | var validationResults = new HashSet(); 25 | 26 | foreach (var validator in _validators) 27 | { 28 | var res = validator.Validate(context); 29 | validationResults.Add(res); 30 | } 31 | 32 | return new ValidationResult(validationResults.SelectMany(vr => vr.Errors)); 33 | } 34 | 35 | public async Task ValidateAsync(IValidationContext context, CancellationToken cancellation = new CancellationToken()) 36 | { 37 | var validationTasks = new List>(); 38 | 39 | foreach (var validator in _validators) 40 | { 41 | var validationTask = validator.ValidateAsync(context, cancellation); 42 | validationTasks.Add(validationTask); 43 | } 44 | 45 | var validationResults = await Task.WhenAll(validationTasks); 46 | 47 | return new ValidationResult(validationResults.SelectMany(vr => vr.Errors)); 48 | } 49 | 50 | public IValidatorDescriptor CreateDescriptor() 51 | { 52 | throw new NotImplementedException(); 53 | } 54 | 55 | public bool CanValidateInstancesOfType(Type type) 56 | { 57 | throw new NotImplementedException(); 58 | } 59 | 60 | public ValidationResult Validate(T instance) 61 | { 62 | var failures = _validators 63 | .Select(v => v.Validate(instance)) 64 | .SelectMany(validationResult => validationResult.Errors) 65 | .Where(f => f != null) 66 | .ToList(); 67 | 68 | return new ValidationResult(failures); 69 | } 70 | 71 | public async Task ValidateAsync(T instance, CancellationToken cancellation = new CancellationToken()) 72 | { 73 | var validationTasks = new List>(); 74 | 75 | foreach (var validator in _validators) 76 | { 77 | var validationTask = validator.ValidateAsync(instance, cancellation); 78 | validationTasks.Add(validationTask); 79 | } 80 | 81 | var validationResults = await Task.WhenAll(validationTasks); 82 | 83 | return new ValidationResult(validationResults.SelectMany(vr => vr.Errors)); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/HttpClients/OpenMovieDbClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Web; 8 | using KesselRun.Business.DataTransferObjects.Media; 9 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping; 10 | using KesselRunFramework.AspNet.Infrastructure.HttpClient; 11 | using KesselRunFramework.Core.Infrastructure.Extensions; 12 | using KesselRunFramework.Core.Infrastructure.Http; 13 | using KesselRunFramework.Core.Infrastructure.Validation; 14 | using Microsoft.AspNetCore.Mvc; 15 | 16 | namespace KesselRun.Web.Api.HttpClients 17 | { 18 | public class OpenMovieDbClient : TypedClientBase, ITypedHttpClient 19 | { 20 | public OpenMovieDbClient(HttpClient httpClient) 21 | : base(httpClient) 22 | { 23 | HttpClient.BaseAddress = new Uri("https://www.omdbapi.com"); 24 | UriBuilder = new UriBuilder(HttpClient.BaseAddress); 25 | QueryStringParams = HttpUtility.ParseQueryString(UriBuilder.Query); 26 | QueryStringParams["apikey"] = "[api key]"; // your api key goes here. 27 | } 28 | 29 | 30 | public async Task> GetShow(string title, int season, CancellationToken cancellationToken) 31 | { 32 | QueryStringParams["t"] = title; 33 | QueryStringParams["Season"] = season.ToString(); 34 | 35 | UriBuilder.Query = QueryStringParams.ToString(); 36 | 37 | HttpStatusCode statusCode = default(HttpStatusCode); 38 | 39 | try 40 | { 41 | using (var response = await HttpClient.GetAsync(UriBuilder.Uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) 42 | { 43 | statusCode = response.StatusCode; 44 | response.EnsureSuccessStatusCode(); 45 | 46 | var stream = await response.Content.ReadAsStreamAsync(); 47 | 48 | return await DeserializeAsync(stream); 49 | } 50 | } 51 | catch 52 | { 53 | if (statusCode == HttpStatusCode.InternalServerError) 54 | { 55 | var problemDetails = new ProblemDetails(); 56 | problemDetails.Extensions.Add("HttpClientException", "An error was thrown from an API being called."); 57 | problemDetails.Title = ProblemDetailTitles.InternalServerError; 58 | problemDetails.Type = ProblemDetailTypes.InternalServerError; 59 | problemDetails.Status = (int)HttpStatusCode.InternalServerError; 60 | 61 | return problemDetails; 62 | } 63 | 64 | if (statusCode == HttpStatusCode.Unauthorized) 65 | { 66 | var problemDetails = new ProblemDetails(); 67 | problemDetails.Extensions.Add("UnauthorisedException", "A 401 Unauthorized response was received from an API being called."); 68 | problemDetails.Title = ProblemDetailTitles.AuthorizationError; 69 | problemDetails.Type = ProblemDetailTypes.Unauthorized; 70 | problemDetails.Status = (int) HttpStatusCode.Unauthorized; 71 | 72 | return problemDetails; 73 | } 74 | 75 | throw; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/HttpClients/WeatherClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | using KesselRun.Business.DataTransferObjects.Weather; 10 | using KesselRunFramework.AspNet.Infrastructure.HttpClient; 11 | using KesselRunFramework.Core.Infrastructure.Errors; 12 | using KesselRunFramework.Core.Infrastructure.Logging; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace KesselRun.Web.Api.HttpClients 16 | { 17 | public class WeatherClient : TypedClientBase, ITypedHttpClient 18 | { 19 | private readonly ILogger _logger; 20 | public string City { get; set; } 21 | public string Units { get; set; } 22 | 23 | public WeatherClient(ILogger logger, HttpClient httpClient) 24 | : base(httpClient) 25 | { 26 | _logger = logger; 27 | HttpClient.BaseAddress = new Uri("https://api.openweathermap.org/data/2.5/weather"); 28 | UriBuilder = new UriBuilder(HttpClient.BaseAddress); 29 | QueryStringParams = HttpUtility.ParseQueryString(UriBuilder.Query); 30 | QueryStringParams["appid"] = "[app id]"; // your app id goes here. 31 | } 32 | 33 | public async Task GetWeather(CancellationToken cancellationToken) 34 | { 35 | QueryStringParams["q"] = City; 36 | QueryStringParams["units"] = Units; 37 | 38 | UriBuilder.Query = QueryStringParams.ToString(); 39 | 40 | WeatherDto weather = null; 41 | 42 | try 43 | { 44 | using (var response = await HttpClient.GetAsync(UriBuilder.Uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) 45 | { 46 | if (!response.IsSuccessStatusCode) 47 | { 48 | if (response.StatusCode == HttpStatusCode.UnprocessableEntity) 49 | { 50 | var errorStream = await response.Content.ReadAsStreamAsync(cancellationToken); 51 | 52 | // Do something with errors here. The following code is just a guide and incomplete 53 | 54 | using (var streamReader = new StreamReader(errorStream, Encoding.UTF8, true)) 55 | { 56 | //using (var jsonTextReader = new JsonTextReader(streamReader)) 57 | { 58 | //var jsonSerializer = new JsonSerializer(); 59 | //'var validationErrors = await JsonSerializer.DeserializeAsync(errorStream); 60 | } 61 | } 62 | } 63 | } 64 | 65 | response.EnsureSuccessStatusCode(); 66 | 67 | var stream = await response.Content.ReadAsStreamAsync(cancellationToken); 68 | weather = await DeserializeAsync(stream); 69 | } 70 | } 71 | catch (Exception exception) 72 | { 73 | _logger.LogError(EventIDs.EventIdHttpClient, 74 | exception, 75 | MessageTemplates.HttpClientGet, 76 | UriBuilder.Uri.AbsolutePath 77 | ); 78 | } 79 | 80 | return weather; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Logging/LoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace KesselRunFramework.AspNet.Infrastructure.Logging 6 | { 7 | public static class LoggingExtensions 8 | { 9 | private static readonly Action BeforeValidatingMessageTrace; 10 | private static readonly Action>, string, Exception> InvalidMessageTrace; 11 | private static readonly Action ModelBinderUsed; 12 | private static readonly Action ProfileMessagingTrace; 13 | private static readonly Action ValidMessageTrace; 14 | 15 | 16 | const string PipelineBehaviour = "Pipeline Behaviour: "; 17 | 18 | static LoggingExtensions() 19 | { 20 | BeforeValidatingMessageTrace = LoggerMessage.Define( 21 | LogLevel.Debug, 22 | new EventId((int)TraceEventIdentifiers.BeforeValidatingMessageTrace, nameof(TraceBeforeValidatingMessage)), 23 | PipelineBehaviour + " Validating Message." 24 | ); 25 | 26 | ProfileMessagingTrace = LoggerMessage.Define( 27 | LogLevel.Debug, 28 | new EventId((int)TraceEventIdentifiers.ProfileMessagingTrace, nameof(TraceMessageProfiling)), 29 | PipelineBehaviour + " Request took {milliseconds} milliseconds." 30 | ); 31 | 32 | InvalidMessageTrace = LoggerMessage.Define>, string>( 33 | LogLevel.Debug, 34 | new EventId((int)TraceEventIdentifiers.InValidMessageTrace, nameof(TraceMessageValidationFailed)), 35 | PipelineBehaviour + " Invalid Message. {errors}. User: {user}" 36 | ); 37 | 38 | ModelBinderUsed = LoggerMessage.Define( 39 | LogLevel.Debug, 40 | new EventId((int)TraceEventIdentifiers.ModelBinderUsedTrace, nameof(TraceMessageModelBinderUsed)), 41 | "Parameter '{modelName}' of type '{type}' bound using ModelBinder \"{modelBinder}\"." 42 | ); 43 | 44 | ValidMessageTrace = LoggerMessage.Define( 45 | LogLevel.Debug, 46 | new EventId((int)TraceEventIdentifiers.ValidMessageTrace, nameof(TraceMessageValidationPassed)), 47 | PipelineBehaviour + " Message is valid." 48 | ); 49 | } 50 | 51 | public static void TraceMessageProfiling(this ILogger logger, long milliseconds) 52 | { 53 | ProfileMessagingTrace(logger, milliseconds, null); 54 | } 55 | 56 | public static void TraceMessageValidationFailed(this ILogger logger, IReadOnlyDictionary> errors, string user) 57 | { 58 | InvalidMessageTrace(logger, errors, user, null); 59 | } 60 | 61 | public static void TraceBeforeValidatingMessage(this ILogger logger) 62 | { 63 | BeforeValidatingMessageTrace(logger, null); 64 | } 65 | 66 | public static void TraceMessageModelBinderUsed(this ILogger logger, string parameter, string type, string modelBinder) 67 | { 68 | ModelBinderUsed(logger, parameter, type, modelBinder, null); 69 | } 70 | 71 | public static void TraceMessageValidationPassed(this ILogger logger) 72 | { 73 | ValidMessageTrace(logger, null); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /KesselRunFramework.DataAccess/Ops/SimpleListRetriever.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using KesselRunFramework.DataAccess.Domain; 5 | using Microsoft.Data.SqlClient; 6 | 7 | namespace KesselRunFramework.DataAccess.Ops 8 | { 9 | public class SimpleListRetriever : ISimpleListRetriever 10 | { 11 | private readonly IDbResolver _dbResolver; 12 | 13 | public SimpleListRetriever(IDbResolver dbResolver) 14 | { 15 | _dbResolver = dbResolver; 16 | } 17 | 18 | public IEnumerable GetSimpleList(string query, SqlParameter[] parameters = null, bool? withNullCheck = null) 19 | { 20 | var simpleList = new HashSet(); 21 | using (var conn = _dbResolver.GetDbConnectionManager().GetOpenConnection()) 22 | using (var dataReader = conn.ExecuteCommandQuery(query, parameters)) 23 | { 24 | if (withNullCheck.HasValue && withNullCheck.Value) 25 | { 26 | while (dataReader.Read()) 27 | { 28 | if (dataReader.IsDBNull(0) || dataReader.IsDBNull(1)) 29 | continue; 30 | 31 | simpleList.Add(new SimpleListItemObject 32 | { 33 | Id = dataReader.GetInt32(0), 34 | Name = dataReader.GetString(1) 35 | }); 36 | } 37 | } 38 | else 39 | { 40 | while (dataReader.Read()) 41 | { 42 | simpleList.Add(new SimpleListItemObject 43 | { 44 | Id = dataReader.GetInt32(0), 45 | Name = dataReader.GetString(1) 46 | }); 47 | } 48 | } 49 | } 50 | 51 | return simpleList; 52 | } 53 | 54 | 55 | public async Task> GetSimpleListAsync(string query, CancellationToken cancellationToken, SqlParameter[] parameters = null, bool? withNullCheck = null) 56 | { 57 | var simpleList = new HashSet(); 58 | using (var conn = _dbResolver.GetDbConnectionManager().GetOpenConnection()) 59 | using (var dataReader = await conn.ExecuteCommandQueryAsync(query, cancellationToken, parameters)) 60 | { 61 | if (withNullCheck.HasValue && withNullCheck.Value) 62 | { 63 | while (dataReader.Read()) 64 | { 65 | if (dataReader.IsDBNull(0) || dataReader.IsDBNull(1)) 66 | continue; 67 | 68 | simpleList.Add(new SimpleListItemObject 69 | { 70 | Id = dataReader.GetInt32(0), 71 | Name = dataReader.GetString(1) 72 | }); 73 | } 74 | } 75 | else 76 | { 77 | while (dataReader.Read()) 78 | { 79 | simpleList.Add(new SimpleListItemObject 80 | { 81 | Id = dataReader.GetInt32(0), 82 | Name = dataReader.GetString(1) 83 | }); 84 | } 85 | } 86 | } 87 | 88 | return simpleList; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Messaging/Pipelines/BusinessValidationPipeline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentValidation; 7 | using FluentValidation.AspNetCore; 8 | using KesselRunFramework.AspNet.Infrastructure; 9 | using KesselRunFramework.AspNet.Infrastructure.Logging; 10 | using KesselRunFramework.Core.Infrastructure.Extensions; 11 | using KesselRunFramework.Core.Infrastructure.Invariants; 12 | using KesselRunFramework.Core.Infrastructure.Messaging; 13 | using MediatR; 14 | using Microsoft.AspNetCore.Mvc.Infrastructure; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace KesselRunFramework.AspNet.Messaging.Pipelines 18 | { 19 | public class BusinessValidationPipeline : IPipelineBehavior 20 | where TResponse : class 21 | where TRequest : IRequest, IValidateable 22 | { 23 | private readonly IValidator _compositeValidator; 24 | private readonly ILogger _logger; 25 | private readonly ICurrentUser _currentUser; 26 | private readonly IActionContextAccessor _actionContextAccessor; 27 | 28 | public BusinessValidationPipeline( 29 | IValidator compositeValidator, 30 | ILogger logger, 31 | ICurrentUser currentUser, 32 | IActionContextAccessor actionContextAccessor) 33 | { 34 | _compositeValidator = compositeValidator; 35 | _logger = logger; 36 | _currentUser = currentUser; 37 | _actionContextAccessor = actionContextAccessor; 38 | } 39 | 40 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 41 | { 42 | _logger.TraceBeforeValidatingMessage(); 43 | 44 | var responseType = typeof(TResponse); 45 | 46 | if (responseType.Name.StartsWith(nameof(ValidateableResponse), StringComparison.Ordinal)) 47 | { 48 | var result = await _compositeValidator.ValidateAsync(request, cancellationToken); 49 | 50 | if (!result.IsValid) 51 | { 52 | var errorsCollated = result.ToDictionary(); 53 | 54 | _logger.TraceMessageValidationFailed(errorsCollated, _currentUser?.UserName ?? GeneralPurpose.AnonymousUser); 55 | 56 | // Add validation fail to ModelState to make it available there. Just in case it is needed. 57 | result.AddToModelState(_actionContextAccessor.ActionContext.ModelState, string.Empty); 58 | 59 | // Deal with type depending on whether it is the generic version of ValidateableResponse or not. 60 | var resultType = responseType.GetGenericArguments().FirstOrDefault(); 61 | 62 | if (ReferenceEquals(resultType, null)) 63 | { 64 | var nonGenericInvalidResponse = new ValidateableResponse(errorsCollated) as TResponse; 65 | 66 | return nonGenericInvalidResponse; 67 | } 68 | 69 | var invalidResponseType = typeof(ValidateableResponse<>).MakeGenericType(resultType); 70 | 71 | var invalidResponse = 72 | Activator.CreateInstance( 73 | invalidResponseType, 74 | null, 75 | errorsCollated 76 | ) as TResponse; 77 | 78 | return invalidResponse; 79 | } 80 | 81 | _logger.TraceMessageValidationPassed(); 82 | 83 | return await next(); 84 | } 85 | 86 | throw new Exception($"IValidateable implementation must be a {nameof(ValidateableResponse)}."); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Middleware/ApiExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Mime; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping; 8 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 9 | using KesselRunFramework.AspNet.Response; 10 | using KesselRunFramework.Core.Infrastructure.Errors; 11 | using KesselRunFramework.Core.Infrastructure.Extensions; 12 | using KesselRunFramework.Core.Infrastructure.Logging; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.Extensions.Logging; 15 | 16 | namespace KesselRunFramework.AspNet.Middleware 17 | { 18 | public class ApiExceptionMiddleware 19 | { 20 | private readonly RequestDelegate _next; 21 | private readonly ILogger _logger; 22 | private readonly ApiExceptionOptions _options; 23 | 24 | public ApiExceptionMiddleware( 25 | ApiExceptionOptions options, 26 | RequestDelegate next, 27 | ILogger logger) 28 | { 29 | _next = next; 30 | _logger = logger; 31 | _options = options; 32 | } 33 | 34 | public async Task Invoke(HttpContext context /* other dependencies */) 35 | { 36 | try 37 | { 38 | await _next(context); 39 | } 40 | catch (Exception ex) 41 | { 42 | await HandleExceptionAsync(context, ex, _options); 43 | } 44 | } 45 | 46 | private async Task HandleExceptionAsync(HttpContext context, Exception exception, ApiExceptionOptions apiExceptionOptions) 47 | { 48 | var errorId = Guid.NewGuid().ToString(); 49 | 50 | // This is what we tell the client. 51 | var outcome = OperationOutcome.UnSuccessfulOutcome; 52 | outcome.ErrorId = errorId; 53 | 54 | #if DEBUG 55 | outcome.Message = Errors.UnhandledErrorDebug.FormatAs(exception.GetBaseException().Message); 56 | outcome.Errors = exception.StackTrace.Split( 57 | Environment.NewLine, StringSplitOptions.RemoveEmptyEntries 58 | ).Select(str => str.Trim()); // ease of readability, for debugging purposes 👍 59 | #else 60 | outcome.Message = Errors.UnhandledError.FormatAs(errorId); 61 | #endif 62 | 63 | 64 | apiExceptionOptions.AddResponseDetails?.Invoke(context, exception, outcome); 65 | 66 | // This is what we log. 67 | var resolvedExceptionMessage = GetInnermostExceptionMessage(exception); 68 | 69 | var level = _options.DetermineLogLevel?.Invoke(exception) ?? LogLevel.Error; 70 | 71 | _logger.Log(level, 72 | EventIDs.EventIdUncaughtGlobal, 73 | exception, 74 | MessageTemplates.UncaughtGlobal, 75 | resolvedExceptionMessage, 76 | errorId 77 | ); 78 | 79 | var apiResponse = new ApiResponse 80 | { 81 | Data = null, 82 | Outcome = outcome 83 | }; 84 | 85 | var result = JsonSerializer.Serialize(apiResponse, Common.JsonSerializerOptions); 86 | context.Response.ContentType = MediaTypeNames.Application.Json; 87 | context.Response.StatusCode = StatusCodes.Status500InternalServerError; 88 | 89 | await context.Response.WriteAsync(result); 90 | } 91 | 92 | private string GetInnermostExceptionMessage(Exception exception) 93 | { 94 | if (ReferenceEquals(exception.InnerException, null)) 95 | return exception.Message; 96 | 97 | return GetInnermostExceptionMessage(exception.InnerException); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /KesselRun.Examples.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KesselRun.Web.Api", "KesselRun.Web.Api\KesselRun.Web.Api.csproj", "{6F0E5508-11DF-45D4-AEBC-BC08DE588E2E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KesselRunFramework.Core", "KesselRunFramework.Core\KesselRunFramework.Core.csproj", "{E6773E9F-9BDB-4B85-A3F2-D8958044FA2C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KesselRunFramework.DataAccess", "KesselRunFramework.DataAccess\KesselRunFramework.DataAccess.csproj", "{E5F16F88-B0D0-435A-AACF-48E64073BFA0}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KesselRunFramework.AspNet", "KesselRunFramework.AspNet\KesselRunFramework.AspNet.csproj", "{6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KesselRun.Business", "KesselRun.Business\KesselRun.Business.csproj", "{94CB80BB-930E-45E0-AE24-842E9C0635D9}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02 Business", "02 Business", "{91A3F9AD-E9B4-40BD-B916-C32EE6C9C3E4}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{27598526-C352-46BF-A4D9-04D966489A16}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01 Framework", "01 Framework", "{8E520D2F-3FD1-42DA-A0BC-FA4D708E8144}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {6F0E5508-11DF-45D4-AEBC-BC08DE588E2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {6F0E5508-11DF-45D4-AEBC-BC08DE588E2E}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {6F0E5508-11DF-45D4-AEBC-BC08DE588E2E}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {6F0E5508-11DF-45D4-AEBC-BC08DE588E2E}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E6773E9F-9BDB-4B85-A3F2-D8958044FA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E6773E9F-9BDB-4B85-A3F2-D8958044FA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E6773E9F-9BDB-4B85-A3F2-D8958044FA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E6773E9F-9BDB-4B85-A3F2-D8958044FA2C}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {E5F16F88-B0D0-435A-AACF-48E64073BFA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {E5F16F88-B0D0-435A-AACF-48E64073BFA0}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {E5F16F88-B0D0-435A-AACF-48E64073BFA0}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {E5F16F88-B0D0-435A-AACF-48E64073BFA0}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {94CB80BB-930E-45E0-AE24-842E9C0635D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {94CB80BB-930E-45E0-AE24-842E9C0635D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {94CB80BB-930E-45E0-AE24-842E9C0635D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {94CB80BB-930E-45E0-AE24-842E9C0635D9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {6F0E5508-11DF-45D4-AEBC-BC08DE588E2E} = {27598526-C352-46BF-A4D9-04D966489A16} 54 | {E6773E9F-9BDB-4B85-A3F2-D8958044FA2C} = {8E520D2F-3FD1-42DA-A0BC-FA4D708E8144} 55 | {E5F16F88-B0D0-435A-AACF-48E64073BFA0} = {8E520D2F-3FD1-42DA-A0BC-FA4D708E8144} 56 | {6A3C3C5E-7A60-4FE7-B106-33F8C7EE1EAD} = {8E520D2F-3FD1-42DA-A0BC-FA4D708E8144} 57 | {94CB80BB-930E-45E0-AE24-842E9C0635D9} = {91A3F9AD-E9B4-40BD-B916-C32EE6C9C3E4} 58 | EndGlobalSection 59 | GlobalSection(ExtensibilityGlobals) = postSolution 60 | SolutionGuid = {52A53FA9-34C0-4E8F-8CD7-3A7B49047566} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ModelBinders/UtcAwareDateTimeModelBinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading.Tasks; 4 | using KesselRunFramework.AspNet.Infrastructure.Logging; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace KesselRunFramework.AspNet.Infrastructure.ModelBinders 10 | { 11 | /// 12 | /// Starting point for this class taken from this Github issue comment by Damien Edwards (member of the ASP.NET Core team): 13 | /// https://github.com/dotnet/aspnetcore/issues/11584#issuecomment-506007647 14 | /// 15 | public class UtcAwareDateTimeModelBinder : IModelBinder 16 | { 17 | private readonly ILogger _logger; 18 | 19 | public UtcAwareDateTimeModelBinder(ILoggerFactory loggerFactory) 20 | { 21 | if (loggerFactory == null) 22 | { 23 | throw new ArgumentNullException(nameof(loggerFactory)); 24 | } 25 | 26 | _logger = loggerFactory.CreateLogger(); 27 | } 28 | 29 | public Task BindModelAsync(ModelBindingContext bindingContext) 30 | { 31 | if (bindingContext == null) 32 | { 33 | throw new ArgumentNullException(nameof(bindingContext)); 34 | } 35 | 36 | _logger.AttemptToBindModel(bindingContext); 37 | 38 | var modelName = bindingContext.ModelName; 39 | var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); 40 | 41 | if (valueProviderResult == ValueProviderResult.None) 42 | { 43 | _logger.ValueNotFoundInRequest(bindingContext); 44 | 45 | // no entry 46 | _logger.AttemptToBindModelDone(bindingContext); 47 | 48 | return Task.CompletedTask; 49 | } 50 | 51 | var modelState = bindingContext.ModelState; 52 | modelState.SetModelValue(modelName, valueProviderResult); 53 | 54 | var metadata = bindingContext.ModelMetadata; 55 | var type = metadata.UnderlyingOrModelType; 56 | 57 | try 58 | { 59 | var value = valueProviderResult.FirstValue; 60 | 61 | object model; 62 | if (string.IsNullOrWhiteSpace(value)) 63 | { 64 | model = null; 65 | } 66 | else if (type == typeof(DateTime)) 67 | { 68 | if (value.EndsWith("Z")) 69 | { 70 | model = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces); 71 | } 72 | else 73 | { 74 | model = DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces); 75 | } 76 | } 77 | else 78 | { 79 | // should be unreachable 80 | throw new NotSupportedException(); 81 | } 82 | 83 | // When converting value, a null model may indicate a failed conversion for an otherwise required 84 | // model (can't set a ValueType to null). This detects if a null model value is acceptable given the 85 | // current bindingContext. If not, an error is logged. 86 | if (model == null && !metadata.IsReferenceOrNullableType) 87 | { 88 | modelState.TryAddModelError( 89 | modelName, 90 | metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor( 91 | valueProviderResult.ToString() 92 | ) 93 | ); 94 | } 95 | else 96 | { 97 | bindingContext.Result = ModelBindingResult.Success(model); 98 | } 99 | } 100 | catch (Exception exception) 101 | { 102 | // Conversion failed. 103 | modelState.TryAddModelError(modelName, exception, metadata); 104 | } 105 | 106 | _logger.AttemptToBindModelDone(bindingContext); 107 | 108 | return Task.CompletedTask; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Controllers/AppApiController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using KesselRunFramework.AspNet.Infrastructure.Extensions; 4 | using KesselRunFramework.AspNet.Response; 5 | using KesselRunFramework.Core.Infrastructure.Messaging; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace KesselRunFramework.AspNet.Infrastructure.Controllers 10 | { 11 | [ApiController] 12 | public class AppApiController : ControllerBase 13 | { 14 | protected readonly ICurrentUser _currentUser; 15 | 16 | public AppApiController(ICurrentUser currentUser) 17 | { 18 | _currentUser = currentUser; 19 | } 20 | 21 | public IActionResult OkResponse(T data, string message = null) 22 | { 23 | var outcome = OperationOutcome.SuccessfulOutcome; 24 | outcome.Message = message ?? string.Empty; 25 | 26 | var apiResponse = new ApiResponse 27 | { 28 | Data = data, 29 | Outcome = outcome 30 | }; 31 | 32 | return Ok(apiResponse); 33 | } 34 | 35 | public IActionResult OkResponse(T data, OperationOutcome operationOutcome) 36 | { 37 | var apiResponse = new ApiResponse 38 | { 39 | Data = data, 40 | Outcome = operationOutcome 41 | }; 42 | 43 | return Ok(apiResponse); 44 | } 45 | 46 | public IActionResult OkResponse(ApiResponse apiResponse) 47 | { 48 | return Ok(apiResponse); 49 | } 50 | 51 | public IActionResult BadRequestResponse(T data, string errorMessage = null, dynamic errors = null) 52 | { 53 | var outcome = OperationOutcome.ValidationFailOutcome(errors, errorMessage); 54 | 55 | var apiResponse = new ApiResponse 56 | { 57 | Data = data, 58 | Outcome = outcome 59 | }; 60 | 61 | return BadRequest(apiResponse); 62 | } 63 | 64 | public IActionResult BadRequestResponse(T data, OperationOutcome operationOutcome) 65 | { 66 | var apiResponse = new ApiResponse 67 | { 68 | Data = data, 69 | Outcome = operationOutcome 70 | }; 71 | 72 | return BadRequest(apiResponse); 73 | } 74 | 75 | public IActionResult BadRequestResponse(ApiResponse apiResponse) 76 | { 77 | return BadRequest(apiResponse); 78 | } 79 | 80 | public IActionResult CreatedResponse(T data, string url, string message = null) 81 | { 82 | var outcome = OperationOutcome.SuccessfulOutcome; 83 | outcome.Message = message ?? string.Empty; 84 | 85 | var apiResponse = new ApiResponse 86 | { 87 | Data = data, 88 | Outcome = outcome 89 | }; 90 | 91 | return Created(url, apiResponse); 92 | } 93 | 94 | public IActionResult CreatedResponse(T data, string url, OperationOutcome operationOutcome) 95 | { 96 | var apiResponse = new ApiResponse 97 | { 98 | Data = data, 99 | Outcome = operationOutcome 100 | }; 101 | 102 | return Created(url, apiResponse); 103 | } 104 | 105 | public IActionResult CreatedResponse(string url, ApiResponse apiResponse) 106 | { 107 | return Created(url, apiResponse); 108 | } 109 | 110 | public IActionResult UnprocessableEntityResponse(T data, string errorMessage = null, dynamic errors = null) 111 | { 112 | var outcome = OperationOutcome.ValidationFailOutcome(errors, errorMessage); 113 | 114 | var apiResponse = new ApiResponse 115 | { 116 | Data = data, 117 | Outcome = outcome 118 | }; 119 | 120 | return StatusCode(StatusCodes.Status422UnprocessableEntity, apiResponse); 121 | } 122 | 123 | public IActionResult UnprocessableEntityResponse(T data, OperationOutcome operationOutcome) 124 | { 125 | var apiResponse = new ApiResponse 126 | { 127 | Data = data, 128 | Outcome = operationOutcome 129 | }; 130 | 131 | return StatusCode(StatusCodes.Status422UnprocessableEntity, apiResponse); 132 | } 133 | 134 | public IActionResult UnprocessableEntityResponse(ApiResponse apiResponse) 135 | { 136 | return StatusCode(StatusCodes.Status422UnprocessableEntity, apiResponse); 137 | } 138 | 139 | public IActionResult InternalServerErrorResponse(T data, string errorMessage = null, dynamic errors = null) 140 | { 141 | var outcome = OperationOutcome.ValidationFailOutcome(errors, errorMessage); 142 | 143 | var apiResponse = new ApiResponse 144 | { 145 | Data = data, 146 | Outcome = outcome 147 | }; 148 | 149 | return this.ServerError(apiResponse); 150 | } 151 | 152 | public IActionResult InternalServerErrorResponse(T data, OperationOutcome operationOutcome) 153 | { 154 | var apiResponse = new ApiResponse 155 | { 156 | Data = data, 157 | Outcome = operationOutcome 158 | }; 159 | 160 | return this.ServerError(apiResponse); 161 | } 162 | 163 | public IActionResult InternalServerErrorResponse(ApiResponse apiResponse) 164 | { 165 | return this.ServerError(apiResponse); 166 | } 167 | 168 | protected void PrepareInvalidResult(ValidateableResponse result) 169 | where T : class 170 | { 171 | // The ModelState may be valid at this point, BUT the result has validation errors (possibly from the Business layer) 172 | if (!result.IsValidResponse && ModelState.IsValid) result.AddToModelState(ModelState); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/ActionFilters/ApiExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 5 | using KesselRunFramework.AspNet.Response; 6 | using KesselRunFramework.Core.Infrastructure.Errors; 7 | using KesselRunFramework.Core.Infrastructure.Errors.Api; 8 | using KesselRunFramework.Core.Infrastructure.Invariants; 9 | using KesselRunFramework.Core.Infrastructure.Logging; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.AspNetCore.Mvc.Filters; 13 | using Microsoft.Extensions.Logging; 14 | using Serilog.Context; 15 | 16 | namespace KesselRunFramework.AspNet.Infrastructure.ActionFilters 17 | { 18 | public class ApiExceptionFilter : ExceptionFilterAttribute 19 | { 20 | private readonly ILogger _logger; 21 | 22 | public ApiExceptionFilter(ILoggerFactory loggerFactory) 23 | { 24 | _logger = loggerFactory.CreateLogger(); 25 | } 26 | 27 | public override void OnException(ExceptionContext context) 28 | { 29 | // The operationOutcome is what we want to actually show the client. 30 | // Obviously, we do not want to display a Stacktrace or Exception details. 31 | // This does not deal with ModelState errors, as their is a separate ActionFilter for that. 32 | var operationOutcome = default(OperationOutcome); 33 | context.RouteData.Values.TryGetValue(Invariants.AspNet.Mvc.Controller, out var controller); 34 | context.RouteData.Values.TryGetValue(Invariants.AspNet.Mvc.Action, out var action); 35 | 36 | if (context.Exception is ApiValidationException apiValidationException) 37 | { 38 | operationOutcome = GetClientErrorPayload(Origin.ValidationError, apiValidationException); 39 | 40 | context.Exception = null; 41 | context.HttpContext.Response.StatusCode = StatusCodes.Status422UnprocessableEntity; 42 | } 43 | else 44 | { 45 | using (LogContext.PushProperty(Invariants.Logging.LogContexts.FromRoute, 46 | string.Concat(controller?.ToString() ?? string.Empty, GeneralPurpose.UniqueDelimiter, action?.ToString() ?? string.Empty))) 47 | { 48 | if (context.Exception is ApiException apiException) 49 | { 50 | // handle explicit 'known' API errors 51 | context.Exception = null; 52 | 53 | context.HttpContext.Response.StatusCode = apiException.StatusCode; 54 | 55 | operationOutcome = GetClientErrorPayload(Origin.Action, apiException); 56 | 57 | var level = apiException.ExceptionGravity == ExceptionGravity.Error 58 | ? LogLevel.Error 59 | : LogLevel.Critical; 60 | 61 | _logger.Log(level, 62 | EventIDs.EventIdAppThrown, 63 | apiException, 64 | MessageTemplates.DefaultLog, 65 | apiException.Message, 66 | operationOutcome.ErrorId 67 | ); 68 | } 69 | else 70 | { 71 | var exception = context.Exception; 72 | 73 | // Unhandled errors 74 | operationOutcome = GetClientErrorPayload(Origin.Unhandled, exception); 75 | 76 | context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; 77 | 78 | _logger.LogError(EventIDs.EventIdUncaught, 79 | exception, 80 | MessageTemplates.DefaultLog, 81 | operationOutcome.Message, 82 | operationOutcome.ErrorId 83 | ); 84 | 85 | context.Exception = null; 86 | } 87 | } 88 | } 89 | 90 | var wrappedPayload = new ApiResponse> 91 | { 92 | Data = null, 93 | Outcome = operationOutcome 94 | }; 95 | 96 | // always return a JSON result 97 | context.Result = new JsonResult(wrappedPayload); 98 | 99 | base.OnException(context); 100 | } 101 | 102 | private OperationOutcome GetClientErrorPayload(Origin origin, Exception exception) 103 | { 104 | var operationOutcome = default(OperationOutcome); 105 | 106 | switch (origin) 107 | { 108 | case Origin.ValidationError: 109 | operationOutcome = OperationOutcome.ValidationFailOutcome( 110 | ((ApiValidationException)exception).Errors, 111 | Errors.ValidationFailure 112 | ); 113 | break; 114 | 115 | case Origin.Action: 116 | case Origin.Unhandled: 117 | 118 | operationOutcome = OperationOutcome.UnSuccessfulOutcome; 119 | operationOutcome.ErrorId = Guid.NewGuid().ToString(); 120 | 121 | #if DEBUG 122 | operationOutcome.Message = string.Format(Errors.UnhandledErrorDebug, exception.GetBaseException().Message); 123 | operationOutcome.Errors = exception.StackTrace.Split( 124 | Environment.NewLine, StringSplitOptions.RemoveEmptyEntries 125 | ).Select(str => str.Trim()); // ease of readability, for debugging purposes 👍 126 | #else 127 | operationOutcome.Message = string.Format(Errors.UnhandledError, operationOutcome.ErrorId); 128 | #endif 129 | 130 | break; 131 | 132 | default: 133 | throw new ArgumentOutOfRangeException(nameof(origin), origin, null); 134 | } 135 | 136 | return operationOutcome; 137 | } 138 | 139 | private enum Origin 140 | { 141 | Action, 142 | ValidationError, 143 | Unhandled 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /KesselRun.Web.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using AutoMapper; 6 | using FluentValidation.AspNetCore; 7 | using KesselRun.Business.DataTransferObjects; 8 | using KesselRun.Business.Validation; 9 | using KesselRun.Web.Api.Infrastructure.Bootstrapping; 10 | using KesselRun.Web.Api.Infrastructure.Mapping; 11 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config; 12 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Ioc; 13 | using KesselRunFramework.AspNet.Infrastructure.HttpClient; 14 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 15 | using KesselRunFramework.AspNet.Messaging.Pipelines; 16 | using KesselRunFramework.AspNet.Middleware; 17 | using KesselRunFramework.Core.Infrastructure.Extensions; 18 | using KesselRunFramework.Core.Infrastructure.Http; 19 | using Microsoft.AspNetCore.Builder; 20 | using Microsoft.AspNetCore.Hosting; 21 | using Microsoft.Extensions.Configuration; 22 | using Microsoft.Extensions.DependencyInjection; 23 | using Serilog; 24 | using SimpleInjector; 25 | 26 | namespace KesselRun.Web.Api 27 | { 28 | public class Startup 29 | { 30 | private readonly Container Container = new Container(); 31 | 32 | public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment) 33 | { 34 | Configuration = configuration; 35 | WebHostEnvironment = webHostEnvironment; 36 | Assemblies = GetAssemblies(); 37 | } 38 | 39 | public IConfiguration Configuration { get; } 40 | public IEnumerable Versions { get; set; } 41 | public IWebHostEnvironment WebHostEnvironment { get; } 42 | public IEnumerable ExportedTypesWebAssembly { get; set; } 43 | IDictionary Assemblies { get; set; } 44 | public AppConfiguration AppConfiguration { get; set; } 45 | 46 | public void ConfigureServices(IServiceCollection services) 47 | { 48 | services.AddControllers(MvcConfigurer.ConfigureMvcOptions) 49 | .ConfigureApiBehaviorOptions(ApiBehaviourConfigurer.ConfigureApiBehaviour) 50 | .AddJsonOptions(JsonOptionsConfigurer.ConfigureJsonOptions) 51 | .AddFluentValidation(fv => 52 | fv.RegisterValidatorsFromAssemblyContaining(lifetime: ServiceLifetime.Singleton) 53 | ); 54 | 55 | AppConfiguration = StartupConfigurer.GetAppConfiguration(Configuration); 56 | 57 | Versions = AppConfiguration.GeneralConfig.OpenApiInfoList.Select(i => i.Version); // stash this for use in the Configure method below. 58 | services.AddAppApiVersioning().AddSwagger(WebHostEnvironment, Configuration, AppConfiguration.GeneralConfig.OpenApiInfoList); 59 | 60 | services.ConfigureAppServices(WebHostEnvironment, Container); 61 | 62 | ExportedTypesWebAssembly = Assemblies[StartUpConfig.Executing].GetExportedTypes(); 63 | 64 | var httpClientTypes = ExportedTypesWebAssembly 65 | .Where(t => t.IsClass && typeof(ITypedHttpClient).IsAssignableFrom(t)); 66 | 67 | //services.AddRedirect(WebHostEnvironment, GeneralConfig.Hsts.MaxAge); 68 | 69 | services.RegisterTypedHttpClients(httpClientTypes); 70 | 71 | } 72 | 73 | 74 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 75 | { 76 | app.UseSimpleInjectorForDomain(Container); 77 | 78 | RegisterApplicationServices(); 79 | 80 | app.ConfigureMiddlewareForEnvironments(env, Container); 81 | 82 | app.UseApiExceptionHandler(opts => 83 | { 84 | opts.AddResponseDetails = OptionsDelegates.UpdateApiErrorResponse; 85 | opts.DetermineLogLevel = OptionsDelegates.DetermineLogLevel; 86 | }); 87 | 88 | app.UseHttpsRedirection(); 89 | 90 | app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = RequestLoggingConfigurer.EnrichFromRequest); 91 | 92 | app.UseSwaggerInDevAndStaging(WebHostEnvironment, Versions.ToArray()); 93 | 94 | app.UseRouting(); 95 | 96 | app.UseAuthorization(); 97 | 98 | app.UseSession(); 99 | 100 | app.UseEndpoints(endpoints => 101 | { 102 | endpoints.MapControllers(); 103 | }); 104 | } 105 | 106 | private void RegisterApplicationServices() 107 | { 108 | Container.RegisterSingleton(); 109 | 110 | Container.RegisterValidationAbstractions(new[] { Assemblies[StartUpConfig.Executing], Assemblies[StartUpConfig.Domain] }); 111 | Container.RegisterAutomapperAbstractions(GetAutoMapperProfiles(Assemblies)); 112 | Container.RegisterMediatRAbstractions(new[] { Assemblies[StartUpConfig.Executing] }, GetTypesForPipeline(WebHostEnvironment)); 113 | Container.RegisterApplicationServices(Assemblies[StartUpConfig.Domain], Configuration, "KesselRun.Business.ApplicationServices"); 114 | } 115 | 116 | private static Type[] GetTypesForPipeline(IWebHostEnvironment webHostEnvironment) 117 | { 118 | return webHostEnvironment.IsDevelopmentOrIsStaging() 119 | ? new[] 120 | { 121 | typeof(OperationProfilingPipeline<,>), // not for Production 122 | typeof(LogContextPipeline<,>), 123 | typeof(BusinessValidationPipeline<,>) 124 | } 125 | : new[] 126 | { 127 | typeof(LogContextPipeline<,>), 128 | typeof(BusinessValidationPipeline<,>) 129 | }; 130 | } 131 | 132 | private static IDictionary GetAssemblies() 133 | { 134 | var assemblies = new Dictionary(StringComparer.Ordinal) 135 | { 136 | {StartUpConfig.Executing, typeof(Startup).GetTypeInfo().Assembly}, 137 | {StartUpConfig.Domain, typeof(RegisterUserPayloadDto).GetTypeInfo().Assembly } 138 | }; 139 | 140 | // include any custom (domain) assemblies which will require scanning as part of the startup process. 141 | 142 | return assemblies; 143 | } 144 | 145 | private static Profile[] GetAutoMapperProfiles(IDictionary configurationAssemblies) 146 | { 147 | var kesselRunApiProfile = new KesselRunApiProfile("KesselRunApiProfile"); 148 | kesselRunApiProfile.InitializeMappings(configurationAssemblies[StartUpConfig.Domain].InArray()); 149 | 150 | return kesselRunApiProfile.InArray(); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /KesselRunFramework.AspNet/Infrastructure/Bootstrapping/Config/VersioningExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config.SwaggerFilters; 6 | using KesselRunFramework.AspNet.Infrastructure.Invariants; 7 | using KesselRunFramework.Core.Infrastructure.Extensions; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.AspNetCore.Mvc.Versioning; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.OpenApi.Models; 15 | using Swashbuckle.AspNetCore.SwaggerGen; 16 | 17 | namespace KesselRunFramework.AspNet.Infrastructure.Bootstrapping.Config 18 | { 19 | public static class VersioningExtensions 20 | { 21 | public static IServiceCollection AddAppApiVersioning(this IServiceCollection services, Action options = null) 22 | { 23 | if (services == null) throw new ArgumentNullException(nameof(services)); 24 | 25 | services.AddApiVersioning(options ?? (o => 26 | { 27 | o.DefaultApiVersion = new ApiVersion(StartUpConfig.MajorVersion1, StartUpConfig.MinorVersion0); // specify the default api version 28 | o.AssumeDefaultVersionWhenUnspecified = true; // assume that the caller wants the default version if they don't specify 29 | o.ReportApiVersions = true; // add headers in responses which show supported/deprecated versions 30 | })); 31 | 32 | return services; 33 | } 34 | 35 | public static IServiceCollection AddSwagger(this IServiceCollection services, 36 | IWebHostEnvironment hostingEnvironment, 37 | IConfiguration configuration, 38 | IList openApiInfos) 39 | { 40 | if (services == null) throw new ArgumentNullException(nameof(services)); 41 | if (hostingEnvironment == null) throw new ArgumentNullException(nameof(hostingEnvironment)); 42 | if (configuration == null) throw new ArgumentNullException(nameof(configuration)); 43 | 44 | if (hostingEnvironment.IsDevelopmentOrIsStaging()) 45 | { 46 | services.AddSwaggerGen(c => 47 | { 48 | for (int i = 0; i < openApiInfos.Count; i++) 49 | { 50 | var name = string.Concat(Swagger.Versions.VersionPrefix, openApiInfos[i].Version); 51 | c.SwaggerDoc(name, openApiInfos[i]); 52 | } 53 | 54 | // This line of code is to accommodate the fact that some of the classes have the same name (but are in different namespaces). 55 | // Swashbuckle uses the class name as the SchemaId by default. If 2 classes have the same name, it goes 💣! 56 | // This code includes the namespace, so there is no longer any conflict in SchemaIds. 57 | c.CustomSchemaIds(x => x.FullName); 58 | 59 | // This filter removed the "version" parameter from the parameters textboxes which are rendered. 60 | // It is redundant and not a genuine parameter if using Urls for versioning. 61 | c.OperationFilter(); 62 | 63 | // This filter adds the version to the paths rendered on the page so you can see what Version you are dealing with at a glance. 64 | c.DocumentFilter(); 65 | 66 | // This predicate is used by convention to obviate the need to decorate relevant actions with an 67 | // ApiExplorerSettings attribute (setting the GroupName). That attribute is required where there 68 | // are different versions of the same action i.e. same name. 69 | c.DocInclusionPredicate((docName, apiDesc) => 70 | { 71 | if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; 72 | 73 | var versions = methodInfo.DeclaringType 74 | .GetCustomAttributes(true) 75 | .OfType() 76 | .SelectMany(attr => attr.Versions); 77 | 78 | var maps = apiDesc.CustomAttributes() 79 | .OfType() 80 | .SelectMany(attr => attr.Versions) 81 | .ToArray(); 82 | 83 | return versions.Any(v => $"{Swagger.Versions.VersionPrefix}{v.ToString()}" == docName) && 84 | ( 85 | maps.Length == 0 || 86 | maps.Any(v => $"{Swagger.Versions.VersionPrefix}{v.ToString()}" == docName) 87 | ); 88 | }); 89 | 90 | c.AddSecurityDefinition(Invariants.Identity.Bearer, new OpenApiSecurityScheme 91 | { 92 | Description = Swagger.SecurityDefinition.Description, 93 | In = ParameterLocation.Header, 94 | Name = Swagger.SecurityDefinition.Name, 95 | Type = SecuritySchemeType.ApiKey, 96 | Scheme = Invariants.Identity.Bearer 97 | }); 98 | 99 | c.AddSecurityRequirement(new OpenApiSecurityRequirement 100 | { 101 | { 102 | new OpenApiSecurityScheme 103 | { 104 | Reference = new OpenApiReference 105 | { 106 | Type = ReferenceType.SecurityScheme, 107 | Id = Invariants.Identity.Bearer 108 | }, 109 | Scheme = Invariants.Identity.Oauth2, 110 | Name = Invariants.Identity.Bearer, 111 | In = ParameterLocation.Header 112 | }, 113 | new List() 114 | } 115 | }); 116 | 117 | c.OperationFilter(); 118 | 119 | c.DescribeAllParametersInCamelCase(); 120 | 121 | //c.IncludeXmlComments(XmlCommentsFilePath); 122 | 123 | }); 124 | } 125 | return services; 126 | } 127 | 128 | public static void UseSwaggerInDevAndStaging(this IApplicationBuilder app, IWebHostEnvironment hostingEnvironment, string[] versions) 129 | { 130 | if (hostingEnvironment.IsDevelopmentOrIsStaging()) 131 | { 132 | app.UseSwagger(); 133 | 134 | app.UseSwaggerUI(c => 135 | { 136 | for (int i = 0; i < versions.Length; i++) 137 | { 138 | c.DefaultModelsExpandDepth(-1); // ◀ This prevents the rendering of the Models section at the bottom 139 | c.SwaggerEndpoint( 140 | Swagger.EndPoint.Url.FormatAs(string.Concat(Swagger.Versions.VersionPrefix, versions[i])), 141 | Swagger.EndPoint.Name.FormatAs(string.Concat(Swagger.Versions.VersionPrefix, versions[i])) 142 | ); 143 | } 144 | }); 145 | } 146 | } 147 | } 148 | } 149 | --------------------------------------------------------------------------------