Repository: dotnetjunkie/solidservices Branch: master Commit: 2d7c3de74877 Files: 131 Total size: 201.7 KB Directory structure: gitextract_3fjw8qzp/ ├── .gitignore ├── LICENSE ├── README.md └── src/ ├── .gitignore ├── BusinessLayer/ │ ├── BusinessLayer.csproj │ ├── BusinessLayerBootstrapper.cs │ ├── CommandHandlers/ │ │ ├── CreateOrderCommandHandler.cs │ │ └── ShipOrderCommandHandler.cs │ ├── CrossCuttingConcerns/ │ │ ├── AuthorizationCommandHandlerDecorator.cs │ │ ├── AuthorizationQueryHandlerDecorator.cs │ │ ├── DataAnnotationsValidator.cs │ │ ├── StructuredLoggingCommandHandlerDecorator.cs │ │ ├── StructuredLoggingQueryHandlerDecorator.cs │ │ ├── StructuredMessageLogger.cs │ │ ├── ValidationCommandHandlerDecorator.cs │ │ └── ValidationQueryHandlerDecorator.cs │ ├── Helpers/ │ │ └── PagingExtensions.cs │ ├── ICommandHandler.cs │ ├── ILogger.cs │ ├── IQueryHandler.cs │ ├── IValidator.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── QueryHandlers/ │ │ ├── GetOrderByIdQueryHandler.cs │ │ └── GetUnshippedOrdersQueryHandler.cs │ └── packages.config ├── Client/ │ ├── App.config │ ├── Bootstrapper.cs │ ├── Client.csproj │ ├── Code/ │ │ ├── CommandServiceClient.cs │ │ ├── DynamicQueryProcessor.cs │ │ ├── QueryServiceClient.cs │ │ ├── WcfServiceCommandHandlerProxy.cs │ │ └── WcfServiceQueryHandlerProxy.cs │ ├── Controllers/ │ │ ├── CommandExampleController.cs │ │ └── QueryExampleController.cs │ ├── CrossCuttingConcerns/ │ │ └── FromWcfFaultTranslatorCommandHandlerDecorator.cs │ ├── ICommandHandler.cs │ ├── IQueryHandler.cs │ ├── Program.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Wcf/ │ │ ├── KnownCommandTypesAttribute.cs │ │ ├── KnownQueryAndResultTypesAttribute.cs │ │ ├── KnownTypesAttribute.cs │ │ └── KnownTypesDataContractResolver.cs │ └── packages.config ├── Contract/ │ ├── Commands/ │ │ └── Orders/ │ │ ├── CreateOrder.cs │ │ └── ShipOrder.cs │ ├── Contract.csproj │ ├── DTOs/ │ │ ├── Address.cs │ │ └── OrderInfo.cs │ ├── ICommand.cs │ ├── IQuery.cs │ ├── IQueryProcessor.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Queries/ │ │ ├── Orders/ │ │ │ ├── GetOrderById.cs │ │ │ └── GetUnshippedOrders.cs │ │ ├── PageInfo.cs │ │ └── Paged.cs │ └── Validators/ │ ├── CompositeValidationResult.cs │ ├── NonEmptyGuidAttribute.cs │ └── ValidateObjectAttribute.cs ├── Settings.StyleCop ├── SolidServices.sln ├── WcfService/ │ ├── Bootstrapper.cs │ ├── Code/ │ │ ├── DebugLogger.cs │ │ └── WcfExceptionTranslator.cs │ ├── CommandService.svc │ ├── CommandService.svc.cs │ ├── CrossCuttingConcerns/ │ │ └── ToWcfFaultTranslatorCommandHandlerDecorator.cs │ ├── Global.asax │ ├── Global.asax.cs │ ├── NonDotNetQueryService.cs │ ├── NonDotNetQueryService.svc │ ├── NonDotNetQueryService.tt │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── QueryService.svc │ ├── QueryService.svc.cs │ ├── ValidationError.cs │ ├── WcfService.csproj │ ├── Web.Debug.config │ ├── Web.Release.config │ ├── Web.config │ └── packages.config ├── WebApiService/ │ ├── App_Data/ │ │ └── .gitignore │ ├── App_Start/ │ │ ├── FilterConfig.cs │ │ ├── RouteConfig.cs │ │ ├── SwaggerConfig.cs │ │ └── WebApiConfig.cs │ ├── Bootstrapper.cs │ ├── Code/ │ │ ├── CommandDelegatingHandler.cs │ │ ├── ExampleObjectCreator.cs │ │ ├── QueryDelegatingHandler.cs │ │ ├── SerializationHelpers.cs │ │ └── WebApiExceptionTranslator.cs │ ├── Global.asax │ ├── Global.asax.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Web.Debug.config │ ├── Web.Release.config │ ├── Web.config │ ├── WebApiService.csproj │ └── packages.config ├── WebCore3Service/ │ ├── Bootstrapper.cs │ ├── Code/ │ │ ├── CommandHandlerMiddleware.cs │ │ ├── HeaderDictionaryExtensions.cs │ │ ├── HttpContextExtensions.cs │ │ ├── QueryHandlerMiddleware.cs │ │ ├── SerializationHelpers.cs │ │ ├── StreamExtensions.cs │ │ └── WebApiErrorResponseBuilder.cs │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WebCore3Service.csproj │ ├── appsettings.Development.json │ └── appsettings.json └── WebCore6Service/ ├── Bootstrapper.cs ├── Code/ │ ├── Commands.cs │ ├── FlatApiMessageMappingBuilder.cs │ ├── IMessageMappingBuilder.cs │ ├── MessageMapping.cs │ ├── MessageMappingExtensions.cs │ ├── Queries.cs │ └── WebApiErrorResponseBuilder.cs ├── Program.cs ├── Properties/ │ └── launchSettings.json ├── Swagger/ │ ├── SwaggerExtensions.cs │ └── XmlDocumentationTypeDescriptionProvider.cs ├── WebCore6Service.csproj ├── appsettings.Development.json └── appsettings.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ################################################################################ # This .gitignore file was automatically created by Microsoft(R) Visual Studio. ################################################################################ /.vs ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 Steven van Deursen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Highly Maintainable Web Services The Highly Maintainable Web Services project is a reference architecture application for .NET that demonstrates how to build highly maintainable web services. This project contains no documentation, just code. Please visit the following article for the reasoning behind this project: * [Writing Highly Maintainable WCF services](https://blogs.cuttingedge.it/steven/posts/2012/writing-highly-maintainable-wcf-services/) For more background about the used design, please read the following articles: * [Meanwhile… on the command side of my architecture](https://blogs.cuttingedge.it/steven/p/commands/) * [Meanwhile… on the query side of my architecture](https://blogs.cuttingedge.it/steven/p/queries/) This project contains the following Web Service projects that all expose the same set of commands and queries: * [WCF](https://github.com/dotnetjunkie/solidservices/tree/master/src/WcfService). This project exposes command and query messages through a WCF SOAP service, while all messages are specified explicitly through a WSDL. This exposes an explicit contract to the client, although serialization and deserialization of messages is quite limited in WCF, which likely causes problems when sending and receiving messages, unless the messages are explicitly designed with the WCF-serialization constraints in mind. The [Client](https://github.com/dotnetjunkie/solidservices/tree/master/src/Client) project sends queries and commands through the WCF Service. Due to the setup, it gives full integration into the WCF pipeline, which includes security, logging, encryption, and authorization. * [ASP.NET 'Classic' 4.8 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebApiService) (includes a Swagger API). This project exposes commands and queries as REST API through the System.Web.Http stack (the 'legacy' ASP.NET Web API) of .NET 4.8. REST makes the contract less explicit, but allows more flexibility over a SOAP service. It uses JSON.NET as serialization mechanism, which allows much flexibility in defining command and query messages. Incoming requests are mapped to HttpMessageHandlers, which dispatch messages to underlying command and query handlers. In doing so, it circumvents a lot of the Web API infrastructure, which means logging and security might need to be dealt with separately. This project uses an external NuGet library to allow exposing its API through an OpenAPI/Swagger interface. * [ASP.NET Core 3.1 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebCore3Service). This project exposes commands and queries as REST API through ASP.NET Core 3.1's Web API. REST makes the contract less explicit but allows more flexibility over a SOAP service. Just as the previous 'classic' Web API project, it uses JSON.NET as serialization mechanism, which allows much flexibility in defining command and query messages. Incoming requests are mapped to specific Middleware, which dispatches messages to underlying command and query handlers. In doing so, it circumvents a lot of the ASP.NET Core Web API infrastructure, which means logging and security might need to be dealt with separately. This project has _no_ support for exposing its API through OpenAPI/Swagger. * [ASP.NET Core 6 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebCore6Service) (includes a Swagger API). This project exposes commands and queries as REST API through ASP.NET Core 6 Minimal API. The project uses .NET 6's System.Text.Json as serialization mechanism, which is the built-in mechanism. It is less flexible compared to JSON.NET, but gives superb performance. This project makes full use of the new Minimal API functionality and maps each query and command to a specific URL. This allows full integration into the ASP.NET Core pipeline, including logging, security, and OpenAPI/Swagger. There is some extra code added to expose command and query XML documentation summaries through as part of the operation's documentation. Due to limitations in the Minimal API framework, queries only support HTTP POST operations. ================================================ FILE: src/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # Security *.snk # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ build/ bld/ [Bb]in/ [Oo]bj/ Help/ *.boltdata/ # Visual Studo 2015 cache/options directory .vs/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding addin-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # Windows Azure Build Output csx/ *.build.csdef # Windows Store app package directory AppPackages/ # Others *.[Cc]ache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt ================================================ FILE: src/BusinessLayer/BusinessLayer.csproj ================================================  Debug AnyCPU 8.0.30703 2.0 {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C} Library Properties BusinessLayer BusinessLayer v4.8 512 SAK SAK SAK SAK true full false bin\Debug\ DEBUG;TRACE prompt 4 true false pdbonly true bin\Release\ TRACE prompt 4 true false ..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll ..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll {DDD88351-9A73-4212-85DC-F769B37D5057} Contract ================================================ FILE: src/BusinessLayer/BusinessLayerBootstrapper.cs ================================================ namespace BusinessLayer { using BusinessLayer.CrossCuttingConcerns; using Contract; using SimpleInjector; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; // This class allows registering all types that are defined in the business layer, and are shared across // all applications that use this layer (WCF and Web API). For simplicity, this class is placed inside // this assembly, but this does couple the business layer assembly to the used container. If this is a // concern, create a specific BusinessLayer.Bootstrap project with this class. public static class BusinessLayerBootstrapper { private static readonly Assembly[] ContractAssemblies = new[] { typeof(IQuery<>).Assembly }; private static readonly Assembly[] BusinessLayerAssemblies = new[] { Assembly.GetExecutingAssembly() }; public static void Bootstrap(Container container) { if (container == null) { throw new ArgumentNullException(nameof(container)); } container.RegisterInstance(new DataAnnotationsValidator()); container.Register(typeof(ICommandHandler<>), BusinessLayerAssemblies); container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>)); container.RegisterDecorator(typeof(ICommandHandler<>), typeof(AuthorizationCommandHandlerDecorator<>)); container.Register(typeof(IQueryHandler<,>), BusinessLayerAssemblies); container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(ValidationQueryHandlerDecorator<,>)); container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(AuthorizationQueryHandlerDecorator<,>)); } public static IEnumerable GetCommandTypes() => from assembly in ContractAssemblies from type in assembly.GetExportedTypes() where typeof(ICommand).IsAssignableFrom(type) where !type.IsAbstract select type; public static Type CreateQueryHandlerType(Type queryType) => typeof(IQueryHandler<,>).MakeGenericType(queryType, DetermineResultTypes(queryType).Single()); public static IEnumerable<(Type QueryType, Type ResultType)> GetQueryTypes() => from assembly in ContractAssemblies from type in assembly.GetExportedTypes() where IsQuery(type) select (type, DetermineResultTypes(type).Single()); public static Type GetQueryResultType(Type queryType) => DetermineResultTypes(queryType).Single(); private static bool IsQuery(Type type) => DetermineResultTypes(type).Any(); private static IEnumerable DetermineResultTypes(Type type) => from interfaceType in type.GetInterfaces() where interfaceType.IsGenericType where interfaceType.GetGenericTypeDefinition() == typeof(IQuery<>) select interfaceType.GetGenericArguments()[0]; } } ================================================ FILE: src/BusinessLayer/CommandHandlers/CreateOrderCommandHandler.cs ================================================ namespace BusinessLayer.CommandHandlers { using Contract; using Contract.Commands.Orders; public class CreateOrderCommandHandler : ICommandHandler { private readonly ILogger logger; public CreateOrderCommandHandler(ILogger logger) { this.logger = logger; } public void Handle(CreateOrder command) { // Do something useful here. this.logger.Log(this.GetType().Name + " has been executed successfully."); } } } ================================================ FILE: src/BusinessLayer/CommandHandlers/ShipOrderCommandHandler.cs ================================================ namespace BusinessLayer.CommandHandlers { using Contract; using Contract.Commands.Orders; public class ShipOrderCommandHandler : ICommandHandler { private readonly ILogger logger; public ShipOrderCommandHandler(ILogger logger) { this.logger = logger; } public void Handle(ShipOrder command) { this.logger.Log("Shipping order " + command.OrderId); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/AuthorizationCommandHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using System.Security; using System.Security.Principal; using Contract; public class AuthorizationCommandHandlerDecorator : ICommandHandler where TCommand : ICommand { private readonly ICommandHandler decoratedHandler; private readonly IPrincipal currentUser; private readonly ILogger logger; public AuthorizationCommandHandlerDecorator(ICommandHandler decoratedHandler, IPrincipal currentUser, ILogger logger) { this.decoratedHandler = decoratedHandler; this.currentUser = currentUser; this.logger = logger; } public void Handle(TCommand query) { this.Authorize(); this.decoratedHandler.Handle(query); } private void Authorize() { // Some useful authorization logic here. if (typeof(TCommand).Namespace.Contains("Admin") && !this.currentUser.IsInRole("Admin")) { throw new SecurityException(); } this.logger.Log("User " + this.currentUser.Identity.Name + " has been authorized to execute " + typeof(TCommand).Name); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/AuthorizationQueryHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using System.Security; using System.Security.Principal; using Contract; public class AuthorizationQueryHandlerDecorator : IQueryHandler where TQuery : IQuery { private readonly IQueryHandler decoratedHandler; private readonly IPrincipal currentUser; private readonly ILogger logger; public AuthorizationQueryHandlerDecorator(IQueryHandler decoratedHandler, IPrincipal currentUser, ILogger logger) { this.decoratedHandler = decoratedHandler; this.currentUser = currentUser; this.logger = logger; } public TResult Handle(TQuery query) { this.Authorize(); return this.decoratedHandler.Handle(query); } private void Authorize() { // Some useful authorization logic here. if (typeof(TQuery).Namespace.Contains("Admin") && !this.currentUser.IsInRole("Admin")) { throw new SecurityException(); } this.logger.Log("User " + this.currentUser.Identity.Name + " has been authorized to execute " + typeof(TQuery).Name); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/DataAnnotationsValidator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using System.ComponentModel.DataAnnotations; using System.Diagnostics; public class DataAnnotationsValidator : IValidator { [DebuggerStepThrough] void IValidator.ValidateObject(object instance) { var context = new ValidationContext(instance, null, null); // Throws an exception when instance is invalid. Validator.ValidateObject(instance, context, validateAllProperties: true); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredLoggingCommandHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using Contract; using System.Diagnostics; public sealed class StructuredLoggingCommandHandlerDecorator : ICommandHandler where TCommand : ICommand { private readonly StructuredMessageLogger logger; private readonly ICommandHandler decoratee; public StructuredLoggingCommandHandlerDecorator( StructuredMessageLogger logger, ICommandHandler decoratee) { this.logger = logger; this.decoratee = decoratee; } public void Handle(TCommand command) { var watch = Stopwatch.StartNew(); this.decoratee.Handle(command); this.logger.Log(command, watch.Elapsed); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredLoggingQueryHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using Contract; using System.Diagnostics; public sealed class StructuredLoggingQueryHandlerDecorator : IQueryHandler where TQuery : IQuery { private readonly StructuredMessageLogger logger; private readonly IQueryHandler decoratee; public StructuredLoggingQueryHandlerDecorator( StructuredMessageLogger logger, IQueryHandler decoratee) { this.logger = logger; this.decoratee = decoratee; } public TResult Handle(TQuery query) { var watch = Stopwatch.StartNew(); var result = this.decoratee.Handle(query); this.logger.Log(query, watch.Elapsed); return result; } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredMessageLogger.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using System; using System.Linq; using System.Reflection; /// /// Logs information about the succesfull execution of a given TMessage, where the used template is specific to /// the TMessage type with its specified properties. For instance, it might log the following: /// /// this.logger.LogInformation( /// "{Message} executed in {Milliseconds} with parameters {OrderId}", /// "GetOrderById", // <-- {Message} /// stopwatch.ElapsedMilliseconds, // <-- {Milliseconds} /// message.OrderId); // <-- {OrderId} /// /// /// public sealed class StructuredMessageLogger { private static readonly string MessageName; private static readonly PropertyInfo[] MessageProperties; private static readonly string LogTemplate; private static readonly Type[] SupportedPropertyTypes = typeof(int).Assembly.GetExportedTypes().Where(t => t.IsPrimitive) .Concat(new[] { typeof(string), typeof(Guid) }) .ToArray(); private readonly ILogger logger; static StructuredMessageLogger() { // PERF: By using a static constructor, initialization is done just once. MessageName = typeof(TMessage).Name; MessageProperties = GetLoggableMessageProperties(); LogTemplate = "{Message} executed in {Milliseconds}"; if (MessageProperties.Length > 0) { LogTemplate += " with parameters " + string.Join(", ", MessageProperties.Select(prop => "{" + prop.Name + "}")); } } public StructuredMessageLogger(ILogger logger) { this.logger = logger; } public void Log(TMessage message, TimeSpan elapsed) { object[] parameters = this.BuildParameters(message, elapsed); this.logger.LogInformation(LogTemplate, parameters); } private object[] BuildParameters(TMessage message, TimeSpan elapsed) { var parameters = new object[MessageProperties.Length + 2]; parameters[0] = MessageName; parameters[1] = (long)elapsed.TotalMilliseconds; for (int i = 0; i < MessageProperties.Length; i++) { PropertyInfo property = MessageProperties[i]; // PERF: PropertyInfo.GetValue is pretty slow. If needed this can be optimized by compiling Expression // trees. parameters[i + 2] = property.GetValue(message); } return parameters; } private static PropertyInfo[] GetLoggableMessageProperties() { // TODO: Filter out unwanted properties (e.g. complex one or one's with sensitive info). You // can do this based on an attribute that you place on the property or only include properties // of certain primitive types (or both). The example here uses a white list of supported types return ( from prop in typeof(TMessage).GetProperties(BindingFlags.Instance | BindingFlags.Public) where SupportedPropertyTypes.Contains(prop.PropertyType) orderby prop.Name // Sorting is important, because ordering is not guaranteed across restarts select prop) .ToArray(); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/ValidationCommandHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using Contract; using System; public class ValidationCommandHandlerDecorator : ICommandHandler where TCommand : ICommand { private readonly IValidator validator; private readonly ICommandHandler handler; public ValidationCommandHandlerDecorator(IValidator validator, ICommandHandler handler) { this.validator = validator; this.handler = handler; } void ICommandHandler.Handle(TCommand command) { if (command == null) throw new ArgumentNullException(nameof(command)); // validate the supplied command. this.validator.ValidateObject(command); // forward the (valid) command to the real command handler. this.handler.Handle(command); } } } ================================================ FILE: src/BusinessLayer/CrossCuttingConcerns/ValidationQueryHandlerDecorator.cs ================================================ namespace BusinessLayer.CrossCuttingConcerns { using System; using Contract; public class ValidationQueryHandlerDecorator : IQueryHandler where TQuery : IQuery { private readonly IValidator validator; private readonly IQueryHandler handler; public ValidationQueryHandlerDecorator(IValidator validator, IQueryHandler handler) { this.validator = validator; this.handler = handler; } TResult IQueryHandler.Handle(TQuery query) { if (query == null) throw new ArgumentNullException(nameof(query)); // validate the supplied command. this.validator.ValidateObject(query); // forward the (valid) command to the real command handler. return this.handler.Handle(query); } } } ================================================ FILE: src/BusinessLayer/Helpers/PagingExtensions.cs ================================================ namespace BusinessLayer { using System.Collections.Generic; using System.Linq; using Contract.Queries; public static class PagingExtensions { /// Apply paging in memory, using LINQ to Objects. /// The type of objects to enumerate. /// The collection /// The optional paging object. When null, the default paging values will be used. /// A paged result. public static Paged Page(this IEnumerable collection, PageInfo paging) { paging = paging ?? new PageInfo(); IEnumerable items = paging.IsSinglePage() ? collection : collection.Skip(paging.PageIndex * paging.PageSize).Take(paging.PageSize); return new Paged { Items = items.ToArray(), Paging = paging }; } /// Apply paging using an efficient database query. /// The type of objects to enumerate. /// The collection /// The optional paging object. When null, the default paging values will be used. /// A paged result. public static Paged Page(this IQueryable collection, PageInfo paging) { paging = paging ?? new PageInfo(); IQueryable items = paging.IsSinglePage() ? collection : collection.Skip(paging.PageIndex * paging.PageSize).Take(paging.PageSize); return new Paged { Items = items.ToArray(), Paging = paging }; } } } ================================================ FILE: src/BusinessLayer/ICommandHandler.cs ================================================ using Contract; namespace BusinessLayer { public interface ICommandHandler where TCommand : ICommand { void Handle(TCommand command); } } ================================================ FILE: src/BusinessLayer/ILogger.cs ================================================ namespace BusinessLayer { public interface ILogger { void Log(string message); } public static class LoggerExtensions { public static void LogInformation(this ILogger logger, string messageTemplate, params object[] parameters) { // TODO: Use real structured logging here. logger.Log(messageTemplate); } } } ================================================ FILE: src/BusinessLayer/IQueryHandler.cs ================================================ namespace Contract { public interface IQueryHandler where TQuery : IQuery { TResult Handle(TQuery query); } } ================================================ FILE: src/BusinessLayer/IValidator.cs ================================================ namespace BusinessLayer { public interface IValidator { /// Validates the given instance. /// The instance to validate. /// Thrown when the instance is a null reference. /// Thrown when the instance is invalid. void ValidateObject(object instance); } } ================================================ FILE: src/BusinessLayer/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("BusinessLayer")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("BusinessLayer")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("06b95749-3b64-47c3-b720-edb06233ed33")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/BusinessLayer/QueryHandlers/GetOrderByIdQueryHandler.cs ================================================ namespace BusinessLayer.QueryHandlers { using System; using System.Collections.Generic; using Contract; using Contract.DTOs; using Contract.Queries.Orders; public class GetOrderByIdQueryHandler : IQueryHandler { public OrderInfo Handle(GetOrderById query) { if (query.OrderId == Guid.Empty) { throw new KeyNotFoundException(); } return new OrderInfo { Id = query.OrderId, CreationDate = DateTime.Today, TotalAmount = 300 }; } } } ================================================ FILE: src/BusinessLayer/QueryHandlers/GetUnshippedOrdersQueryHandler.cs ================================================ namespace BusinessLayer.QueryHandlers { using System; using System.Collections.Generic; using System.Linq; using Contract; using Contract.DTOs; using Contract.Queries; using Contract.Queries.Orders; public class GetUnshippedOrdersQueryHandler : IQueryHandler> { private readonly ILogger logger; public GetUnshippedOrdersQueryHandler(ILogger logger) { this.logger = logger; } public Paged Handle(GetUnshippedOrders query) { this.logger.Log(string.Format("{0} {{ Paging = {{ PageIndex = {1}, PageSize = {2} }} }}", query.GetType().Name, query.Paging?.PageIndex, query.Paging?.PageSize)); return GetAllOrders().Page(query.Paging); } private static IEnumerable GetAllOrders() { var random = new Random(); return from number in Enumerable.Range(1, 100000) select new OrderInfo { Id = Guid.NewGuid(), TotalAmount = random.Next(100, 1000), CreationDate = DateTime.Today.AddDays(-number) }; } } } ================================================ FILE: src/BusinessLayer/packages.config ================================================  ================================================ FILE: src/Client/App.config ================================================ ================================================ FILE: src/Client/Bootstrapper.cs ================================================ namespace Client { using Client.Code; using Client.Controllers; using Client.CrossCuttingConcerns; using Contract; using SimpleInjector; public static class Bootstrapper { private static Container container; public static void Bootstrap() { container = new Container(); container.RegisterInstance(new DynamicQueryProcessor(container)); container.Register(typeof(ICommandHandler<>), typeof(WcfServiceCommandHandlerProxy<>)); container.Register(typeof(IQueryHandler<,>), typeof(WcfServiceQueryHandlerProxy<,>)); container.RegisterDecorator(typeof(ICommandHandler<>), typeof(FromWcfFaultTranslatorCommandHandlerDecorator<>)); container.Register(); container.Register(); container.Verify(); } public static TService GetInstance() where TService : class { return container.GetInstance(); } } } ================================================ FILE: src/Client/Client.csproj ================================================  Debug x86 8.0.30703 2.0 {9562D251-49BE-4650-93BD-FA6D66DBA61C} Exe Properties Client Client v4.8 512 SAK SAK SAK SAK x86 true full false bin\Debug\ DEBUG;TRACE prompt 4 true false x86 pdbonly true bin\Release\ TRACE prompt 4 true false ..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll ..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll {DDD88351-9A73-4212-85DC-F769B37D5057} Contract ================================================ FILE: src/Client/Code/CommandServiceClient.cs ================================================ namespace Client.Code { using System.Diagnostics; using System.ServiceModel; using Client.Wcf; // This service reference is hand-coded. This allows us to use our custom KnownCommandTypesAttribute, // which allows providing WCF with known types at runtime. This prevents us to have to update the client // reference each time a new command is added to the system. [KnownCommandTypes] [ServiceContract( Namespace = "http://www.solid.net/commandservice/v1.0", ConfigurationName = "CommandServices.CommandService")] public interface CommandService { [OperationContract( Action = "http://www.solid.net/commandservice/v1.0/CommandService/Execute", ReplyAction = "http://www.solid.net/commandservice/v1.0/CommandService/ExecuteResponse")] object Execute(object command); } public class CommandServiceClient : ClientBase, CommandService { [DebuggerStepThrough] public object Execute(object command) => this.Channel.Execute(command); } } ================================================ FILE: src/Client/Code/DynamicQueryProcessor.cs ================================================ namespace Client.Code { using System.Diagnostics; using SimpleInjector; using Contract; public sealed class DynamicQueryProcessor : IQueryProcessor { private readonly Container container; public DynamicQueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Execute(IQuery query) { var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = this.container.GetInstance(handlerType); return handler.Handle((dynamic)query); } } } ================================================ FILE: src/Client/Code/QueryServiceClient.cs ================================================ namespace Client.Code { using System.Diagnostics; using System.ServiceModel; using Client.Wcf; // This service reference is hand-coded. This allows us to use the KnownQueryAndResultTypesAttribute, // which allows providing WCF with known types at runtime. This prevents us to have to update the client // reference each time a new command is added to the system. [KnownQueryAndResultTypes] [ServiceContract( Namespace = "http://www.cuttingedge.it/solid/queryservice/v1.0", ConfigurationName = "QueryServices.QueryService")] public interface QueryService { [OperationContract( Action = "http://www.cuttingedge.it/solid/queryservice/v1.0/QueryService/Execute", ReplyAction = "http://www.cuttingedge.it/solid/queryservice/v1.0/QueryService/ExecuteResponse")] object Execute(object query); } public class QueryServiceClient : ClientBase, QueryService { [DebuggerStepThrough] public object Execute(object query) => this.Channel.Execute(query); } } ================================================ FILE: src/Client/Code/WcfServiceCommandHandlerProxy.cs ================================================ namespace Client.Code { using System; using System.Diagnostics; public sealed class WcfServiceCommandHandlerProxy : ICommandHandler { [DebuggerStepThrough] public void Handle(TCommand command) { var service = new CommandServiceClient(); try { service.Execute(command); } finally { try { ((IDisposable)service).Dispose(); } catch { // Against good practice and the Framework Design Guidelines, WCF can throw an // exception during a call to Dispose, which can result in loss of the original exception. // See: https://marcgravell.blogspot.com/2008/11/dontdontuse-using.html // See: https://msdn.microsoft.com/en-us/library/aa355056.aspx } } } } } ================================================ FILE: src/Client/Code/WcfServiceQueryHandlerProxy.cs ================================================ namespace Client.Code { using System; using System.Diagnostics; using Contract; public sealed class WcfServiceQueryHandlerProxy : IQueryHandler where TQuery : IQuery { [DebuggerStepThrough] public TResult Handle(TQuery query) { var service = new QueryServiceClient(); try { return (TResult)service.Execute(query); } finally { try { ((IDisposable)service).Dispose(); } catch { // Against good practice and the Framework Design Guidelines, WCF can throw an // exception during a call to Dispose, which can result in loss of the original exception. // See: https://marcgravell.blogspot.com/2008/11/dontdontuse-using.html // See: https://msdn.microsoft.com/en-us/library/aa355056.aspx } } } } } ================================================ FILE: src/Client/Controllers/CommandExampleController.cs ================================================ namespace Client.Controllers { using System; using Contract.Commands.Orders; using Contract.DTOs; public class CommandExampleController { private readonly ICommandHandler createOrderhandler; private readonly ICommandHandler shipOrderhandler; public CommandExampleController( ICommandHandler createOrderhandler, ICommandHandler shipOrderhandler) { this.createOrderhandler = createOrderhandler; this.shipOrderhandler = shipOrderhandler; } public Guid CreateOrder() { var createOrderCommand = new CreateOrder { NewOrderId = Guid.NewGuid(), ShippingAddress = new Address { Country = "The Netherlands", City = "Nijmegen", Street = ".NET Street" } }; this.createOrderhandler.Handle(createOrderCommand); Console.WriteLine("Order with ID {0} has been created.", createOrderCommand.NewOrderId); return createOrderCommand.NewOrderId; } public void ShipOrder(Guid orderId) { this.shipOrderhandler.Handle(new ShipOrder { OrderId = orderId }); Console.WriteLine("Order with ID {0} is shipped.", orderId); } } } ================================================ FILE: src/Client/Controllers/QueryExampleController.cs ================================================ namespace Client.Controllers { using System; using System.Linq; using Contract; using Contract.DTOs; using Contract.Queries; using Contract.Queries.Orders; public class QueryExampleController { private readonly IQueryProcessor queryProcessor; public QueryExampleController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public void ShowOrders(int pageIndex, int pageSize) { var orders = this.queryProcessor.Execute(new GetUnshippedOrders { Paging = new PageInfo { PageIndex = pageIndex, PageSize = pageSize } }); Console.WriteLine(); Console.WriteLine("Query returned {0} orders: ", orders.Items.Length); foreach (var order in orders.Items) { Console.WriteLine("OrderId: {0}, Amount: {1}, ShipDate: {2:d}", order.Id, order.TotalAmount, order.CreationDate); } Console.WriteLine("Total: " + orders.Items.Sum(order => order.TotalAmount)); } } } ================================================ FILE: src/Client/CrossCuttingConcerns/FromWcfFaultTranslatorCommandHandlerDecorator.cs ================================================ namespace Client.CrossCuttingConcerns { using System.ComponentModel.DataAnnotations; using System.ServiceModel; public class FromWcfFaultTranslatorCommandHandlerDecorator : ICommandHandler { private readonly ICommandHandler decoratee; public FromWcfFaultTranslatorCommandHandlerDecorator(ICommandHandler decoratee) { this.decoratee = decoratee; } public void Handle(TCommand command) { try { this.decoratee.Handle(command); } catch (FaultException ex) when (ex.Code?.Name == "ValidationError") { // The WCF service communicates this specific error back to us in case of a validation // error. We translate it back to an exception that the client can handle.. throw new ValidationException(ex.Message); } } } } ================================================ FILE: src/Client/ICommandHandler.cs ================================================ namespace Client { public interface ICommandHandler { void Handle(TCommand command); } } ================================================ FILE: src/Client/IQueryHandler.cs ================================================ namespace Client { using Contract; public interface IQueryHandler where TQuery : IQuery { TResult Handle(TQuery query); } } ================================================ FILE: src/Client/Program.cs ================================================ namespace Client { using System; using Client.Controllers; public class Program { public static void Main(string[] args) { Bootstrapper.Bootstrap(); var orderController = Bootstrapper.GetInstance(); var orderId = orderController.CreateOrder(); orderController.ShipOrder(orderId); var showUnshippedOrdersController = Bootstrapper.GetInstance(); showUnshippedOrdersController.ShowOrders(pageIndex: 0, pageSize: 10); Console.ReadLine(); } } } ================================================ FILE: src/Client/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Client")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Client")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("3a03603f-acfe-4d73-b924-da926f09a67e")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/Client/Wcf/KnownCommandTypesAttribute.cs ================================================ namespace Client.Wcf { using System; using System.Collections.Generic; using System.Linq; using Contract; public class KnownCommandTypesAttribute : KnownTypesAttribute { public KnownCommandTypesAttribute() : base(new KnownTypesDataContractResolver(CommandTypes)) { } private static IEnumerable CommandTypes => from type in typeof(Contract.Commands.Orders.CreateOrder).Assembly.GetExportedTypes() where typeof(ICommand).IsAssignableFrom(type) where !type.IsAbstract select type; } } ================================================ FILE: src/Client/Wcf/KnownQueryAndResultTypesAttribute.cs ================================================ namespace Client.Wcf { using System; using System.Collections.Generic; using System.Linq; using Contract; public class KnownQueryAndResultTypesAttribute : KnownTypesAttribute { public KnownQueryAndResultTypesAttribute() : base(new KnownTypesDataContractResolver(QueryTypes.Union(ResultTypes))) { } private static IEnumerable ResultTypes => QueryTypes.Select(GetResultType); private static IEnumerable QueryTypes => typeof(Contract.Queries.Orders.GetOrderById).Assembly.GetExportedTypes().Where(IsQueryType); private static bool IsQueryType(Type type) => GetResultType(type) != null; private static Type GetResultType(Type queryType) => ( from iface in queryType.GetInterfaces() where iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IQuery<>) select iface.GetGenericArguments()[0]) .SingleOrDefault(); } } ================================================ FILE: src/Client/Wcf/KnownTypesAttribute.cs ================================================ namespace Client.Wcf { using System; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)] public abstract class KnownTypesAttribute : Attribute, IContractBehavior { private readonly KnownTypesDataContractResolver resolver; public KnownTypesAttribute(KnownTypesDataContractResolver resolver) { this.resolver = resolver; } public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime) { this.CreateMyDataContractSerializerOperationBehaviors(contractDescription); } public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { this.CreateMyDataContractSerializerOperationBehaviors(contractDescription); } public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { } private void CreateMyDataContractSerializerOperationBehaviors(ContractDescription description) { foreach (OperationDescription operationDescription in description.Operations) { this.CreateMyDataContractSerializerOperationBehavior(operationDescription); } } private void CreateMyDataContractSerializerOperationBehavior(OperationDescription operationDescription) { DataContractSerializerOperationBehavior dataContractSerializerOperationbehavior = operationDescription.Behaviors.Find(); dataContractSerializerOperationbehavior.DataContractResolver = this.resolver; } } } ================================================ FILE: src/Client/Wcf/KnownTypesDataContractResolver.cs ================================================ namespace Client.Wcf { using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; using System.Xml; // source: https://msdn.microsoft.com/en-us/library/dd807519%28v=vs.110%29.aspx public sealed class KnownTypesDataContractResolver : DataContractResolver { private readonly Dictionary knownTypes; public KnownTypesDataContractResolver(IEnumerable types) { this.knownTypes = types.Distinct().ToDictionary(GetName); } public override Type ResolveName( string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) { try { Type type; return this.knownTypes.TryGetValue(typeName, out type) ? type : knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null); } catch (Exception ex) { throw new InvalidOperationException( $"Unable to resolve type {typeName}. {ex.InnerException} " + "If the given type name is postfixed with a weird base64 encoded value, it means that " + "the type is a generic type. WCF postfixes the name with a hash based on the " + "namespaces of the generic type arguments. To fix this, mark the class with the " + "DataContractAttribute to force a specific name. Example:" + "[DataContract(Name = nameof(YourType) + \"Of{0}\")]. And don't forget to mark the type's " + "properties with the DataMemberAttribute.", ex); } } [DebuggerStepThrough] public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { if (!knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace)) { typeName = new XmlDictionaryString(XmlDictionary.Empty, type.Name, 0); typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, type.Namespace, 0); } return true; } private static string GetName(Type type) => type.IsArray ? "ArrayOf" + GetName(type.GetElementType()) : type.IsGenericType ? GetGenericName(type) : type.Name; private static string GetGenericName(Type type) { Type typeDef = type.GetGenericTypeDefinition(); string name = typeDef.Name.Substring(0, typeDef.Name.IndexOf('`')); return name + "Of" + string.Join(string.Empty, type.GetGenericArguments().Select(GetName)); } } } ================================================ FILE: src/Client/packages.config ================================================  ================================================ FILE: src/Contract/Commands/Orders/CreateOrder.cs ================================================ namespace Contract.Commands.Orders { using System; using System.ComponentModel.DataAnnotations; using Contract.DTOs; using Contract.Validators; /// Creates a new order. public class CreateOrder : ICommand { /// The order id of the new order. [NonEmptyGuid] public Guid NewOrderId { get; set; } /// The order's shipping address. [Required, ValidateObject] public Address ShippingAddress { get; set; } } } ================================================ FILE: src/Contract/Commands/Orders/ShipOrder.cs ================================================ namespace Contract.Commands.Orders { using System; using Contract.Validators; /// Commands an order to be shipped. public class ShipOrder : ICommand { /// The id of the order. [NonEmptyGuid] public Guid OrderId { get; set; } } } ================================================ FILE: src/Contract/Contract.csproj ================================================  Debug AnyCPU 8.0.30703 2.0 {DDD88351-9A73-4212-85DC-F769B37D5057} Library Properties Contract Contract v4.8 512 SAK SAK SAK SAK true full false bin\Debug\ DEBUG;TRACE prompt 4 false false bin\Debug\Contract.XML pdbonly true bin\Release\ TRACE prompt 4 false false bin\Release\Contract.XML ================================================ FILE: src/Contract/DTOs/Address.cs ================================================ namespace Contract.DTOs { using System.ComponentModel.DataAnnotations; public class Address { /// The country. [Required(AllowEmptyStrings = false)] [StringLength(100)] public string Country { get; set; } /// The city. [Required(AllowEmptyStrings = false)] [StringLength(100)] public string City { get; set; } /// The street name including number. [Required(AllowEmptyStrings = false)] [StringLength(100)] public string Street { get; set; } } } ================================================ FILE: src/Contract/DTOs/OrderInfo.cs ================================================ namespace Contract.DTOs { using System; public class OrderInfo { public Guid Id { get; set; } public DateTime CreationDate { get; set; } public decimal TotalAmount { get; set; } } } ================================================ FILE: src/Contract/ICommand.cs ================================================ namespace Contract { public interface ICommand { } } ================================================ FILE: src/Contract/IQuery.cs ================================================ namespace Contract { /// Defines a query message. /// public interface IQuery { } } ================================================ FILE: src/Contract/IQueryProcessor.cs ================================================ namespace Contract { public interface IQueryProcessor { TResult Execute(IQuery query); } } ================================================ FILE: src/Contract/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Contract")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Contract")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("1281e947-813d-46c8-8b1f-59eba87bb298")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/Contract/Queries/Orders/GetOrderById.cs ================================================ namespace Contract.Queries.Orders { using System; using Contract.DTOs; using Validators; /// Gets order information of a single order by its id. public class GetOrderById : IQuery { /// The id of the order to get. [NonEmptyGuid] public Guid OrderId { get; set; } } } ================================================ FILE: src/Contract/Queries/Orders/GetUnshippedOrders.cs ================================================ namespace Contract.Queries.Orders { using Contract.DTOs; /// /// Gets a paged list of all unshipped orders for the current logged in user. /// public class GetUnshippedOrders : IQuery> { /// The paging information. public PageInfo Paging { get; set; } } } ================================================ FILE: src/Contract/Queries/PageInfo.cs ================================================ namespace Contract.Queries { /// Object containing information about paging. public class PageInfo { /// Returns a PageInfo that represents the request for a single page. public static PageInfo SinglePage() => new PageInfo { PageIndex = 0, PageSize = -1 }; /// The 0-based page index. public int PageIndex { get; set; } /// The number of items in a page. public int PageSize { get; set; } = 20; /// Gets the value indicating whether the page info represents the request for a single page. public bool IsSinglePage() => this.PageIndex == 0 && this.PageSize == -1; } } ================================================ FILE: src/Contract/Queries/Paged.cs ================================================ namespace Contract.Queries { using System.Runtime.Serialization; // Applying the DataContract attribute to generic types prevents WCF from postfixing the closed-generic // type name with a seemingly random hexadecimal code. /// Contains a set of items for the requested page. /// The item type. [DataContract(Name = nameof(Paged) + "Of{0}")] public class Paged { /// Information about the requested page. [DataMember] public PageInfo Paging { get; set; } /// The list of items for the given page. [DataMember] public T[] Items { get; set; } } } ================================================ FILE: src/Contract/Validators/CompositeValidationResult.cs ================================================ namespace Contract.Validators { using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; /// public class CompositeValidationResult : ValidationResult, IEnumerable { public CompositeValidationResult(string errorMessage, IEnumerable results) : base(errorMessage) { this.Results = results; } public IEnumerable Results { get; } public IEnumerator GetEnumerator() => this.Results.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } } ================================================ FILE: src/Contract/Validators/NonEmptyGuidAttribute.cs ================================================ namespace Contract.Validators { using System; using System.ComponentModel.DataAnnotations; /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] public class NonEmptyGuidAttribute : RequiredAttribute { /// public override bool IsValid(object value) { if (value == null) { return false; } if (!(value is Guid)) { return false; } return ((Guid)value) != Guid.Empty; } } } ================================================ FILE: src/Contract/Validators/ValidateObjectAttribute.cs ================================================ namespace Contract.Validators { using System.Collections.Generic; using System.ComponentModel.DataAnnotations; /// public class ValidateObjectAttribute : ValidationAttribute { /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var context = new ValidationContext(value, null, null); var results = new List(); Validator.TryValidateObject(value, context, results, validateAllProperties: true); if (results.Count == 0) { return ValidationResult.Success; } return new CompositeValidationResult( string.Format("Validation for {0} failed!", validationContext.DisplayName), results); } } } ================================================ FILE: src/Settings.StyleCop ================================================ NoMerge is False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False True True False False False False False ================================================ FILE: src/SolidServices.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31919.166 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{9F659AB9-6E1D-47EE-8DF8-EC7C593129D7}" ProjectSection(SolutionItems) = preProject Settings.StyleCop = Settings.StyleCop EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{9562D251-49BE-4650-93BD-FA6D66DBA61C}" ProjectSection(ProjectDependencies) = postProject {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1} = {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1} EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WcfService", "WcfService\WcfService.csproj", "{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contract", "Contract\Contract.csproj", "{DDD88351-9A73-4212-85DC-F769B37D5057}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BusinessLayer", "BusinessLayer\BusinessLayer.csproj", "{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiService", "WebApiService\WebApiService.csproj", "{B71A8419-1827-48A4-913E-467B52A7C2F1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebCore3Service", "WebCore3Service\WebCore3Service.csproj", "{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebCore6Service", "WebCore6Service\WebCore6Service.csproj", "{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Any CPU.ActiveCfg = Debug|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Mixed Platforms.Build.0 = Debug|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|x86.ActiveCfg = Debug|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|x86.Build.0 = Debug|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Any CPU.ActiveCfg = Release|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Mixed Platforms.ActiveCfg = Release|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Mixed Platforms.Build.0 = Release|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|x86.ActiveCfg = Release|x86 {9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|x86.Build.0 = Release|x86 {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|x86.ActiveCfg = Debug|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Any CPU.Build.0 = Release|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|x86.ActiveCfg = Release|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|x86.ActiveCfg = Debug|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Any CPU.ActiveCfg = Release|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Any CPU.Build.0 = Release|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DDD88351-9A73-4212-85DC-F769B37D5057}.Release|x86.ActiveCfg = Release|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|x86.ActiveCfg = Debug|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Any CPU.Build.0 = Release|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|x86.ActiveCfg = Release|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|x86.ActiveCfg = Debug|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Any CPU.Build.0 = Release|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|x86.ActiveCfg = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|x86.ActiveCfg = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|x86.Build.0 = Debug|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Any CPU.Build.0 = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|x86.ActiveCfg = Release|Any CPU {BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|x86.Build.0 = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|x86.ActiveCfg = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|x86.Build.0 = Debug|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Any CPU.Build.0 = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|x86.ActiveCfg = Release|Any CPU {1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {57B78032-8930-4257-B608-F0610294E5DE} EndGlobalSection EndGlobal ================================================ FILE: src/WcfService/Bootstrapper.cs ================================================ namespace WcfService { using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Security.Principal; using System.Threading; using BusinessLayer; using SimpleInjector; using WcfService.Code; using WcfService.CrossCuttingConcerns; public static class Bootstrapper { private static Container container; public static object GetCommandHandler(Type commandType) => container.GetInstance(typeof(ICommandHandler<>).MakeGenericType(commandType)); public static object GetQueryHandler(Type queryType) => container.GetInstance(BusinessLayerBootstrapper.CreateQueryHandlerType(queryType)); public static IEnumerable GetCommandTypes() => BusinessLayerBootstrapper.GetCommandTypes(); public static IEnumerable GetQueryAndResultTypes() { var queryTypes = BusinessLayerBootstrapper.GetQueryTypes().Select(q => q.QueryType); var resultTypes = BusinessLayerBootstrapper.GetQueryTypes().Select(q => q.ResultType).Distinct(); return queryTypes.Concat(resultTypes); } public static void Bootstrap() { container = new Container(); BusinessLayerBootstrapper.Bootstrap(container); container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ToWcfFaultTranslatorCommandHandlerDecorator<>)); container.RegisterWcfServices(Assembly.GetExecutingAssembly()); RegisterWcfSpecificDependencies(); container.Verify(); } public static void Log(Exception ex) { Debug.WriteLine(ex.ToString()); } private static void RegisterWcfSpecificDependencies() { container.RegisterInstance(new DebugLogger()); container.Register(() => Thread.CurrentPrincipal); } } } ================================================ FILE: src/WcfService/Code/DebugLogger.cs ================================================ namespace WcfService.Code { using System.Diagnostics; using BusinessLayer; public sealed class DebugLogger : ILogger { public void Log(string message) { Debug.WriteLine(message); } } } ================================================ FILE: src/WcfService/Code/WcfExceptionTranslator.cs ================================================ namespace WcfService.Code { using System; using System.ComponentModel.DataAnnotations; using System.ServiceModel; public static class WcfExceptionTranslator { public static FaultException CreateFaultExceptionOrNull(Exception exception) { if (exception is ValidationException) { return new FaultException( new ValidationError { ErrorMessage = exception.Message }, exception.Message); } #if DEBUG return new FaultException(exception.ToString()); #else return null; #endif } } } ================================================ FILE: src/WcfService/CommandService.svc ================================================ <%@ ServiceHost Language="C#" Debug="true" Service="WcfService.CommandService" CodeBehind="CommandService.svc.cs" %> ================================================ FILE: src/WcfService/CommandService.svc.cs ================================================ namespace WcfService { using System; using System.Collections.Generic; using System.Reflection; using System.ServiceModel; using Code; [ServiceContract(Namespace = "http://www.solid.net/commandservice/v1.0")] [ServiceKnownType(nameof(GetKnownTypes))] public class CommandService { public static IEnumerable GetKnownTypes(ICustomAttributeProvider provider) => Bootstrapper.GetCommandTypes(); [OperationContract] [FaultContract(typeof(ValidationError))] public void Execute(dynamic command) { try { dynamic commandHandler = Bootstrapper.GetCommandHandler(command.GetType()); commandHandler.Handle(command); } catch (Exception ex) { Bootstrapper.Log(ex); var faultException = WcfExceptionTranslator.CreateFaultExceptionOrNull(ex); if (faultException != null) { throw faultException; } throw; } } } } ================================================ FILE: src/WcfService/CrossCuttingConcerns/ToWcfFaultTranslatorCommandHandlerDecorator.cs ================================================ namespace WcfService.CrossCuttingConcerns { using System.ComponentModel.DataAnnotations; using System.ServiceModel; using BusinessLayer; using Contract; public class ToWcfFaultTranslatorCommandHandlerDecorator : ICommandHandler where TCommand : ICommand { private readonly ICommandHandler decoratee; public ToWcfFaultTranslatorCommandHandlerDecorator(ICommandHandler decoratee) { this.decoratee = decoratee; } public void Handle(TCommand command) { try { this.decoratee.Handle(command); } catch (ValidationException ex) { // This ensures that validation errors are communicated to the client, // while other exceptions are filtered by WCF (if configured correctly). throw new FaultException(ex.Message, new FaultCode("ValidationError")); } } } } ================================================ FILE: src/WcfService/Global.asax ================================================ <%@ Application Codebehind="Global.asax.cs" Inherits="WcfService.Global" Language="C#" %> ================================================ FILE: src/WcfService/Global.asax.cs ================================================ namespace WcfService { using System; public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { Bootstrapper.Bootstrap(); } } } ================================================ FILE: src/WcfService/NonDotNetQueryService.cs ================================================ ================================================ FILE: src/WcfService/NonDotNetQueryService.svc ================================================ <%@ ServiceHost Language="C#" Debug="true" Service="WcfService.NonDotNetQueryService" CodeBehind="NonDotNetQueryService.cs" %> ================================================ FILE: src/WcfService/NonDotNetQueryService.tt ================================================ <# /* Generates a service class for queries to be consumed by non-.NET clients. */ #> <#@ template language="C#" debug="true" hostspecific="true" #> <#@ assembly name="System.Core" #> <#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #> <#@ assembly name="EnvDTE" #> <#@ assembly name="EnvDTE80" #> <#@ assembly name="VSLangProj" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #> <#@ import namespace="EnvDTE" #> <#@ import namespace="EnvDTE80" #> <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> <#@ output extension=".cs" #> <# // To debug, uncomment the next two lines !! // System.Diagnostics.Debugger.Launch(); // System.Diagnostics.Debugger.Break(); Initialize(this); var queryTypes = GetAllQueryTypes(ContractProject).ToArray(); var referencedNamespaces = GetAllReferencedNamespaces(queryTypes); #> // #pragma warning disable 1591 namespace WcfService { using System.ServiceModel; using Contract; <# foreach (var referencedNamespace in referencedNamespaces) { #> using <#=referencedNamespace #>; <#} #> [ServiceContract(Namespace = "http://www.cuttingedge.it/solid/queryservice/v1.0")] public class NonDotNetQueryService { <# foreach (var queryType in queryTypes) { var @interface = GetQueryInterfaceForCodeClass(queryType); var resultType = GetQueryResultType(@interface); #> [OperationContract] [FaultContract(typeof(ValidationError))] public <#= resultType #> <#= queryType.Name.Replace("Query", "") #>(<#= queryType.Name #> query) => Execute(query); <# } #> private static TResult Execute(IQuery query) => (TResult)QueryService.ExecuteQuery(query); } } <#+ const string QueryInterface = "IQuery"; static DTE Dte; static Project CurrentProject; static Project ContractProject; static TextTransformation TT; static string T4FileName; static string T4Folder; // static string GeneratedCode = @"GeneratedCode(""SolidServices"", ""1.0"")"; static Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider(); void Initialize(TextTransformation tt) { TT = tt; T4FileName = Path.GetFileName(Host.TemplateFile); T4Folder = Path.GetDirectoryName(Host.TemplateFile); // Get the DTE service from the host var serviceProvider = Host as IServiceProvider; if (serviceProvider != null) { Dte = serviceProvider.GetService(typeof(SDTE)) as DTE; } // Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe if (Dte == null) { throw new Exception("This template can only be executed through the Visual Studio host"); } CurrentProject = GetProjectContainingT4File(Dte); ContractProject = GetContractProject(Dte); } Project GetProjectContainingT4File(DTE dte) { // Find the .tt file's ProjectItem ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile); // If the .tt file is not opened, open it if (projectItem.Document == null) projectItem.Open(Constants.vsViewKindCode); // Mark the .tt file as unsaved. This way it will be saved and update itself next time the // project is built. Basically, it keeps marking itself as unsaved to make the next build work. // Note: this is certainly hacky, but is the best I could come up with so far. projectItem.Document.Saved = false; return projectItem.ContainingProject; } Project GetContractProject(DTE dte) { string queryCsFile = QueryInterface + ".cs"; ProjectItem projectItem = dte.Solution.FindProjectItem(queryCsFile); if (projectItem == null) { Error("Could not find the VS Project containing the " + queryCsFile + " file."); return null; } return projectItem.ContainingProject; } IEnumerable GetAllQueryTypes(params Project[] projects) { return from Project project in projects from ProjectItem projectItem in project.ProjectItems from type in GetAllQueryTypesRecursive(projectItem) select type; } IEnumerable GetAllQueryTypesRecursive(ProjectItem projectItem) { var queryTypes = GetAllQueryTypes(projectItem); var recursiveQueryTypes = from ProjectItem subItem in projectItem.ProjectItems from type in GetAllQueryTypesRecursive(subItem) select type; return queryTypes.Union(recursiveQueryTypes); } IEnumerable GetAllQueryTypes(ProjectItem projectItem) { if (projectItem.FileCodeModel == null) { return Enumerable.Empty(); } var elements = projectItem.FileCodeModel.CodeElements.OfType().ToArray(); var namespacedTypes = from @namespace in projectItem.FileCodeModel.CodeElements.OfType() from type in @namespace.Members.OfType() select type; var rootTypes = projectItem.FileCodeModel.CodeElements.OfType(); return from type in rootTypes.Union(namespacedTypes) where ImplementsQueryInterface(type) select type; } private string[] GetAllReferencedNamespaces(IEnumerable queryTypes) { return ( from type in queryTypes orderby type.Namespace.Name select type.Namespace.Name) .Distinct() .ToArray(); } private bool ImplementsQueryInterface(CodeClass2 type) { return GetQueryInterfaceForCodeClass(type) != null; } private CodeInterface GetQueryInterfaceForCodeClass(CodeClass2 type) { var queryInterfaces = from implementedInterface in type.ImplementedInterfaces.OfType() where implementedInterface.Name.StartsWith(QueryInterface) select implementedInterface; return queryInterfaces.FirstOrDefault(); } private static readonly int iqueryTypeNameLength = "Contract.IQuery".Length; private static string GetQueryResultType(CodeInterface queryType) { return queryType.FullName.Substring(iqueryTypeNameLength + 1, queryType.FullName.Length - (iqueryTypeNameLength + 2)); } #> ================================================ FILE: src/WcfService/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("WcfService")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("WcfService")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("7f49468b-8e26-4e45-8d87-2de4eccc022b")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/WcfService/QueryService.svc ================================================ <%@ ServiceHost Language="C#" Debug="true" Service="WcfService.QueryService" CodeBehind="QueryService.svc.cs" %> ================================================ FILE: src/WcfService/QueryService.svc.cs ================================================ namespace WcfService { using System; using System.Collections.Generic; using System.Reflection; using System.ServiceModel; using Code; [ServiceContract(Namespace = "http://www.cuttingedge.it/solid/queryservice/v1.0")] [ServiceKnownType(nameof(GetKnownTypes))] public class QueryService { public static IEnumerable GetKnownTypes(ICustomAttributeProvider provider) => Bootstrapper.GetQueryAndResultTypes(); [OperationContract] [FaultContract(typeof(ValidationError))] public object Execute(dynamic query) => ExecuteQuery(query); internal static object ExecuteQuery(dynamic query) { Type queryType = query.GetType(); dynamic queryHandler = Bootstrapper.GetQueryHandler(query.GetType()); try { return queryHandler.Handle(query); } catch (Exception ex) { Bootstrapper.Log(ex); var faultException = WcfExceptionTranslator.CreateFaultExceptionOrNull(ex); if (faultException != null) { throw faultException; } throw; } } } } ================================================ FILE: src/WcfService/ValidationError.cs ================================================ namespace WcfService { public class ValidationError { public string ErrorMessage { get; set; } } } ================================================ FILE: src/WcfService/WcfService.csproj ================================================  Debug AnyCPU 2.0 {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1} {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} Library Properties WcfService WcfService v4.8 true SAK SAK SAK SAK 4.0 true full false bin\ DEBUG;TRACE prompt 4 true false pdbonly true bin\ TRACE prompt 4 true false ..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll ..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll ..\packages\SimpleInjector.Integration.Wcf.5.0.0\lib\net45\SimpleInjector.Integration.Wcf.dll ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll ..\packages\WebActivator.1.4.4\lib\net40\WebActivator.dll Web.config Web.config CommandService.svc Global.asax True True NonDotNetQueryService.tt QueryService.svc TextTemplatingFileGenerator NonDotNetQueryService.cs {5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C} BusinessLayer {DDD88351-9A73-4212-85DC-F769B37D5057} Contract 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) True False 9999 / http://localhost:9998/ False False False ================================================ FILE: src/WcfService/Web.Debug.config ================================================  ================================================ FILE: src/WcfService/Web.Release.config ================================================  ================================================ FILE: src/WcfService/Web.config ================================================  ================================================ FILE: src/WcfService/packages.config ================================================  ================================================ FILE: src/WebApiService/App_Data/.gitignore ================================================ *.xml ================================================ FILE: src/WebApiService/App_Start/FilterConfig.cs ================================================ namespace WebApiService { using System.Web.Mvc; public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } } } ================================================ FILE: src/WebApiService/App_Start/RouteConfig.cs ================================================ namespace WebApiService { using System.Web.Mvc; using System.Web.Routing; public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }); } } } ================================================ FILE: src/WebApiService/App_Start/SwaggerConfig.cs ================================================ [assembly: System.Web.PreApplicationStartMethod(typeof(WebApiService.SwaggerConfig), "Register")] namespace WebApiService { using System; using System.Collections.Generic; using System.Configuration; using System.IO; using System.Linq; using System.Web.Hosting; using System.Web.Http; using System.Web.Http.Description; using Contract.Commands.Orders; using SolidServices.Controllerless.WebApi.Description; using Swashbuckle.Application; using Swashbuckle.Swagger; // NOTE: To see Swagger in action, view this Web API in your browser: http://localhost:2591/swagger/ public static class SwaggerConfig { public static void Register() { var thisAssembly = typeof(SwaggerConfig).Assembly; var contractAssembly = typeof(CreateOrder).Assembly; GlobalConfiguration.Configuration.EnableSwagger(c => { c.SingleApiVersion("v1", "SOLID Services API"); IncludeXmlCommentsFromAppDataFolder(c); }) .EnableSwaggerUi(c => { }); } private static void IncludeXmlCommentsFromAppDataFolder(SwaggerDocsConfig c) { var appDataPath = HostingEnvironment.MapPath("~/App_Data"); // The XML comment files are copied using a post-build event (see project settings / Build Events). string[] xmlCommentsPaths = Directory.GetFiles(appDataPath, "*.xml"); foreach (string xmlCommentsPath in xmlCommentsPaths) { c.IncludeXmlComments(xmlCommentsPath); } var filter = new ControllerlessActionOperationFilter(xmlCommentsPaths); c.OperationFilter(() => filter); if (!xmlCommentsPaths.Any()) { throw new ConfigurationErrorsException("No .xml files were found in the App_Data folder."); } } private sealed class ControllerlessActionOperationFilter : IOperationFilter { private readonly ITypeDescriptionProvider[] providers; public ControllerlessActionOperationFilter(params string[] xmlCommentsPaths) { this.providers = xmlCommentsPaths.Select(p => new XmlDocumentationTypeDescriptionProvider(p)).ToArray(); } public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription) { var descriptor = apiDescription.ActionDescriptor as ControllerlessActionDescriptor; if (descriptor != null) { operation.summary = this.GetSummaries(descriptor.MessageType).FirstOrDefault() ?? operation.summary; } } private IEnumerable GetSummaries(Type type) => from provider in providers let description = provider.GetDescription(type) where description != null select description; } } } ================================================ FILE: src/WebApiService/App_Start/WebApiConfig.cs ================================================ namespace WebApiService { using System.Linq; using System.Net.Http; using System.Web.Http; using System.Web.Http.Cors; using System.Web.Http.Description; using BusinessLayer; using Code; using Newtonsoft.Json.Serialization; using SimpleInjector; using SolidServices.Controllerless.WebApi.Description; public static class WebApiConfig { public static void Register(HttpConfiguration config, Container container) { // Setting the same-origin policy to 'unrestricted'. Remove or change this line if you want to // restrict web pages from making AJAX requests to other domains. For more information, see: // https://docs.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api#scope-rules-for-enablecors config.EnableCors(new EnableCorsAttribute(origins: "*", headers: "*", methods: "*")); UseCamelCaseJsonSerialization(config); #if DEBUG UseIndentJsonSerialization(config); #endif MapRoutes(config, container); UseControllerlessApiDocumentation(config); } private static void MapRoutes(HttpConfiguration config, Container container) { config.Routes.MapHttpRoute( name: "QueryApi", routeTemplate: "api/queries/{query}", defaults: new { }, constraints: new { }, handler: new QueryDelegatingHandler( handlerFactory: container.GetInstance, queryTypes: Bootstrapper.GetKnownQueryTypes())); config.Routes.MapHttpRoute( name: "CommandApi", routeTemplate: "api/commands/{command}", defaults: new { }, constraints: new { }, handler: new CommandDelegatingHandler( handlerFactory: container.GetInstance, commandTypes: Bootstrapper.GetKnownCommandTypes())); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }); } private static void UseControllerlessApiDocumentation(HttpConfiguration config) { var queryApiExplorer = new ControllerlessApiExplorer( messageTypes: Bootstrapper.GetKnownQueryTypes().Select(t => t.QueryType), responseTypeSelector: type => BusinessLayerBootstrapper.GetQueryResultType(type)) { ControllerName = "queries", ParameterSourceSelector = type => ApiParameterSource.FromUri, HttpMethodSelector = type => HttpMethod.Get, ActionNameSelector = type => type.ToFriendlyName() }; var commandApiExplorer = new ControllerlessApiExplorer( messageTypes: Bootstrapper.GetKnownCommandTypes(), responseTypeSelector: type => typeof(void)) { ControllerName = "commands", ParameterName = "command", ActionNameSelector = type => type.ToFriendlyName(), }; config.Services.Replace(typeof(IApiExplorer), new CompositeApiExplorer( config.Services.GetApiExplorer(), commandApiExplorer, queryApiExplorer)); } private static void UseCamelCaseJsonSerialization(HttpConfiguration config) { config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } private static void UseIndentJsonSerialization(HttpConfiguration config) { config.Formatters.JsonFormatter.Indent = true; } } } ================================================ FILE: src/WebApiService/Bootstrapper.cs ================================================ namespace WebApiService { using System; using System.Collections.Generic; using System.Diagnostics; using System.Security.Principal; using System.Threading; using System.Web; using BusinessLayer; using SimpleInjector; using SimpleInjector.Lifestyles; // NOTE: Here are two example urls for queries: // * http://localhost:2591/api/queries/GetUnshippedOrdersForCurrentCustomer?Paging.PageIndex=3&Paging.PageSize=10 // * http://localhost:2591/api/queries/GetOrderById?OrderId=97fc6660-283d-44b6-b170-7db0c2e2afae public static class Bootstrapper { public static IEnumerable GetKnownCommandTypes() => BusinessLayerBootstrapper.GetCommandTypes(); public static IEnumerable<(Type QueryType, Type ResultType)> GetKnownQueryTypes() => BusinessLayerBootstrapper.GetQueryTypes(); public static Container Bootstrap() { var container = new Container(); container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); BusinessLayerBootstrapper.Bootstrap(container); container.RegisterInstance(new HttpContextPrincipal()); container.RegisterInstance(new DebugLogger()); return container; } private sealed class HttpContextPrincipal : IPrincipal { public IIdentity Identity => this.Principal.Identity; private IPrincipal Principal => HttpContext.Current.User ?? Thread.CurrentPrincipal; public bool IsInRole(string role) => this.Principal.IsInRole(role); } private sealed class DebugLogger : ILogger { public void Log(string message) { Debug.WriteLine(message); } } } } ================================================ FILE: src/WebApiService/Code/CommandDelegatingHandler.cs ================================================ namespace WebApiService.Code { using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Threading; using System.Threading.Tasks; using BusinessLayer; using Newtonsoft.Json; using SimpleInjector; public sealed class CommandDelegatingHandler : DelegatingHandler { private readonly Func handlerFactory; private readonly Dictionary commandTypes; public CommandDelegatingHandler(Func handlerFactory, IEnumerable commandTypes) { this.handlerFactory = handlerFactory; this.commandTypes = commandTypes.ToDictionary( keySelector: type => type.ToFriendlyName(), elementSelector: type => type, comparer: StringComparer.OrdinalIgnoreCase); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { string commandName = request.GetRouteData().Values["command"].ToString(); if (request.Method != HttpMethod.Post) { return request.CreateErrorResponse(HttpStatusCode.MethodNotAllowed, "The requested resource does not support HTTP method '" + request.Method + "'."); } if (!this.commandTypes.ContainsKey(commandName)) { return new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound, RequestMessage = request }; } Type commandType = this.commandTypes[commandName]; string commandData = await request.Content.ReadAsStringAsync(); Type handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType); // GetDependencyScope() calls IDependencyResolver.BeginScope internally. request.GetDependencyScope(); this.ApplyHeaders(request); dynamic handler = this.handlerFactory.Invoke(handlerType); try { dynamic command = DeserializeCommand(request, commandData, commandType); handler.Handle(command); return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, RequestMessage = request }; } catch (Exception ex) { var response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(ex, request); if (response != null) { return response; } throw; } } private void ApplyHeaders(HttpRequestMessage request) { // TODO: Here you read the relevant headers and and check them or apply them to the current scope // so the values are accessible during execution of the command. string sessionId = request.Headers.GetValueOrNull("sessionId"); string token = request.Headers.GetValueOrNull("CSRF-token"); } private static object DeserializeCommand(HttpRequestMessage request, string json, Type commandType) => JsonConvert.DeserializeObject(json, commandType, GetJsonFormatter(request).SerializerSettings); private static JsonMediaTypeFormatter GetJsonFormatter(HttpRequestMessage request) => request.GetConfiguration().Formatters.JsonFormatter; } } ================================================ FILE: src/WebApiService/Code/ExampleObjectCreator.cs ================================================ namespace WebApiService.Code { using System; using AutoFixture; using AutoFixture.Kernel; public static class ExampleObjectCreator { public static object Create(Type type) { var fixture = new Fixture(); int index = 1; fixture.Register(() => "sample text " + index++); return new SpecimenContext(fixture).Resolve(type); } } } ================================================ FILE: src/WebApiService/Code/QueryDelegatingHandler.cs ================================================ namespace WebApiService.Code { using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Threading; using System.Threading.Tasks; using System.Web.Http; using Contract; using Newtonsoft.Json; public sealed class QueryDelegatingHandler : DelegatingHandler { private readonly Func handlerFactory; private readonly Dictionary queryTypes; public QueryDelegatingHandler( Func handlerFactory, IEnumerable<(Type QueryType, Type ResultType)> queryTypes) { this.handlerFactory = handlerFactory; this.queryTypes = queryTypes.ToDictionary( keySelector: info => info.QueryType.Name.Replace("Query", string.Empty), elementSelector: info => info, comparer: StringComparer.OrdinalIgnoreCase); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { string queryName = request.GetRouteData().Values["query"].ToString(); if (!this.queryTypes.ContainsKey(queryName)) { return new HttpResponseMessage { StatusCode = HttpStatusCode.NotFound, RequestMessage = request }; } // GET operations get their data through the query string, while POST operations expect a JSON // object being put in the body. string queryData = request.Method == HttpMethod.Get ? SerializationHelpers.ConvertQueryStringToJson(request.RequestUri.Query) : await request.Content.ReadAsStringAsync(); var (queryType, resultType) = this.queryTypes[queryName]; Type handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); // GetDependencyScope() calls IDependencyResolver.BeginScope internally. request.GetDependencyScope(); this.ApplyHeaders(request); dynamic handler = this.handlerFactory.Invoke(handlerType); try { dynamic query = DeserializeQuery(request, queryData, queryType); object result = handler.Handle(query); return CreateResponse(result, resultType, HttpStatusCode.OK, request); } catch (Exception ex) { var response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(ex, request); if (response != null) { return response; } throw; } } private void ApplyHeaders(HttpRequestMessage request) { // TODO: Here you read the relevant headers and check them or apply them to the current scope // so the values are accessible during execution of the query. string sessionId = request.Headers.GetValueOrNull("sessionId"); string token = request.Headers.GetValueOrNull("CSRF-token"); } private static HttpResponseMessage CreateResponse(object data, Type dataType, HttpStatusCode code, HttpRequestMessage request) { var configuration = request.GetConfiguration(); IContentNegotiator negotiator = configuration.Services.GetContentNegotiator(); ContentNegotiationResult result = negotiator.Negotiate(dataType, request, configuration.Formatters); var bestMatchFormatter = result.Formatter; var mediaType = result.MediaType.MediaType; return new HttpResponseMessage { Content = new ObjectContent(dataType, data, bestMatchFormatter, mediaType), StatusCode = code, RequestMessage = request }; } private static dynamic DeserializeQuery(HttpRequestMessage request, string json, Type queryType) => JsonConvert.DeserializeObject(json, queryType, GetJsonFormatter(request).SerializerSettings); private static JsonMediaTypeFormatter GetJsonFormatter(HttpRequestMessage request) => request.GetConfiguration().Formatters.JsonFormatter; } } ================================================ FILE: src/WebApiService/Code/SerializationHelpers.cs ================================================ namespace WebApiService.Code { using System.Collections.Generic; using System.Linq; using System.Web; public static class SerializationHelpers { public static string ConvertQueryStringToJson(string query) { var collection = HttpUtility.ParseQueryString(query); var dictionary = collection.AllKeys.ToDictionary(key => key, key => collection[key]); return ConvertDictionaryToJson(dictionary); } private static string ConvertDictionaryToJson(Dictionary dictionary) { var propertyNames = from key in dictionary.Keys let index = key.IndexOf('.') select index < 0 ? key : key.Substring(0, index); var data = from propertyName in propertyNames.Distinct() let json = dictionary.ContainsKey(propertyName) ? HttpUtility.JavaScriptStringEncode(dictionary[propertyName], true) : ConvertDictionaryToJson(FilterByPropertyName(dictionary, propertyName)) select HttpUtility.JavaScriptStringEncode(propertyName, true) + ": " + json; return "{ " + string.Join(", ", data) + " }"; } private static Dictionary FilterByPropertyName(Dictionary dictionary, string propertyName) { string prefix = propertyName + "."; return dictionary.Keys .Where(key => key.StartsWith(prefix)) .ToDictionary(key => key.Substring(prefix.Length), key => dictionary[key]); } } } ================================================ FILE: src/WebApiService/Code/WebApiExceptionTranslator.cs ================================================ namespace WebApiService.Code { using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security; using Newtonsoft.Json; // Allows translating exceptions thrown by the business layer to HttpResponseExceptions. // This allows returning useful error information to the client. public static class WebApiErrorResponseBuilder { public static HttpResponseMessage CreateErrorResponseOrNull(Exception thrownException, HttpRequestMessage request) { if (thrownException is JsonSerializationException) { // Return when the supplied model (command or query) can't be deserialized. return request.CreateErrorResponse(HttpStatusCode.BadRequest, thrownException.Message); } // Here are some examples of how certain exceptions can be mapped to error responses. if (thrownException is ValidationException) { // Return when the supplied model (command or query) isn't valid. return request.CreateResponse(HttpStatusCode.BadRequest, ((ValidationException)thrownException).ValidationResult); } if (thrownException is OptimisticConcurrencyException) { // Return when there was a concurrency conflict in updating the model. return request.CreateErrorResponse(HttpStatusCode.Conflict, thrownException); } if (thrownException is SecurityException) { // Return when the current user doesn't have the proper rights to execute the requested // operation or to access the requested resource. return request.CreateErrorResponse(HttpStatusCode.Unauthorized, thrownException); } if (thrownException is KeyNotFoundException) { // Return when the requested resource does not exist anymore. Catching a KeyNotFoundException // is an example, but you probably shouldn't throw KeyNotFoundException in this case, since it // could be thrown for other reasons (such as program errors) in which case this branch should // of course not execute. return request.CreateErrorResponse(HttpStatusCode.NotFound, thrownException); } // If the thrown exception can't be handled: return null. return null; } public static string GetValueOrNull(this HttpRequestHeaders headers, string name) { IEnumerable values; return headers.TryGetValues(name, out values) ? values.FirstOrDefault() : null; } } } ================================================ FILE: src/WebApiService/Global.asax ================================================ <%@ Application Codebehind="Global.asax.cs" Inherits="WebApiService.WebApiApplication" Language="C#" %> ================================================ FILE: src/WebApiService/Global.asax.cs ================================================ namespace WebApiService { using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; using SimpleInjector.Integration.WebApi; // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); var container = Bootstrapper.Bootstrap(); container.Verify(); WebApiConfig.Register(GlobalConfiguration.Configuration, container); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); GlobalConfiguration.Configuration.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container); } } } ================================================ FILE: src/WebApiService/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("WebApiService")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("WebApiService")] [assembly: AssemblyCopyright("Copyright © 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("c299c0a5-0aa0-4cf3-87c2-f3b68c5f63ce")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: src/WebApiService/Web.Debug.config ================================================  ================================================ FILE: src/WebApiService/Web.Release.config ================================================  ================================================ FILE: src/WebApiService/Web.config ================================================  ================================================ FILE: src/WebApiService/WebApiService.csproj ================================================  Debug AnyCPU 2.0 {B71A8419-1827-48A4-913E-467B52A7C2F1} {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} Library Properties WebApiService WebApiService v4.8 true true SAK SAK SAK SAK true 4.0 true full false bin\ DEBUG;TRACE prompt 4 true pdbonly true bin\ TRACE prompt 4 true ..\packages\AutoFixture.4.14.0\lib\net452\AutoFixture.dll ..\packages\Fare.2.1.1\lib\net35\Fare.dll ..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll ..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll ..\packages\SimpleInjector.Integration.WebApi.5.0.0\lib\net45\SimpleInjector.Integration.WebApi.dll ..\packages\SolidServices.Controllerless.WebApi.Description.0.1.2\lib\net45\SolidServices.Controllerless.WebApi.Description.dll True ..\packages\Swashbuckle.Core.5.6.0\lib\net40\Swashbuckle.Core.dll ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll True ..\packages\Microsoft.AspNet.WebApi.Client.5.2.6\lib\net45\System.Net.Http.Formatting.dll ..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll True ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll ..\packages\Microsoft.AspNet.Cors.5.2.6\lib\net45\System.Web.Cors.dll ..\packages\Microsoft.AspNet.WebApi.Core.5.2.6\lib\net45\System.Web.Http.dll ..\packages\Microsoft.AspNet.WebApi.Cors.5.2.6\lib\net45\System.Web.Http.Cors.dll ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll True True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.Helpers.dll True ..\packages\Microsoft.AspNet.Mvc.4.0.20710.0\lib\net40\System.Web.Mvc.dll True ..\packages\Microsoft.AspNet.Razor.2.0.20710.0\lib\net40\System.Web.Razor.dll True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.dll True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Deployment.dll True ..\packages\Microsoft.AspNet.WebPages.2.0.20710.0\lib\net40\System.Web.WebPages.Razor.dll False ..\packages\WebActivator.1.4.4\lib\net40\WebActivator.dll ..\packages\WebActivatorEx.2.2.0\lib\net40\WebActivatorEx.dll Global.asax Web.config Web.config {5e9b468d-fd1b-4ae5-8a10-c9b09ce2690c} BusinessLayer {ddd88351-9a73-4212-85dc-f769b37d5057} Contract 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) True True 0 / http://localhost:2591/ False False False copy "$(ProjectDir)..\Contract\bin\$(Configuration)\Contract.xml" "$(ProjectDir)App_Data\Contract.xml" ================================================ FILE: src/WebApiService/packages.config ================================================  ================================================ FILE: src/WebCore3Service/Bootstrapper.cs ================================================ namespace WebCoreService { using System; using System.Collections.Generic; using System.Diagnostics; using System.Security.Principal; using BusinessLayer; using Microsoft.AspNetCore.Http; using SimpleInjector; public static class Bootstrapper { public static IEnumerable GetKnownCommandTypes() => BusinessLayerBootstrapper.GetCommandTypes(); public static IEnumerable<(Type QueryType, Type ResultType)> GetKnownQueryTypes() => BusinessLayerBootstrapper.GetQueryTypes(); public static Container Bootstrap(Container container) { BusinessLayerBootstrapper.Bootstrap(container); container.RegisterSingleton(); container.RegisterInstance(new DebugLogger()); return container; } private sealed class HttpContextPrincipal : IPrincipal { private readonly IHttpContextAccessor httpContextAccessor; public HttpContextPrincipal(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public IIdentity Identity => this.Principal.Identity; public bool IsInRole(string role) => this.Principal.IsInRole(role); private IPrincipal Principal => this.httpContextAccessor.HttpContext.User; } private sealed class DebugLogger : ILogger { public void Log(string message) { Debug.WriteLine(message); } } } } ================================================ FILE: src/WebCore3Service/Code/CommandHandlerMiddleware.cs ================================================ namespace WebCoreService.Code { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BusinessLayer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using SimpleInjector; public sealed class CommandHandlerMiddleware : IMiddleware { private static readonly Dictionary CommandTypes; private readonly Func handlerFactory; private readonly JsonSerializerSettings jsonSettings; static CommandHandlerMiddleware() { CommandTypes = Bootstrapper.GetKnownCommandTypes().ToDictionary( keySelector: type => type.ToFriendlyName(), elementSelector: type => type, comparer: StringComparer.OrdinalIgnoreCase); } public CommandHandlerMiddleware(Container container, JsonSerializerSettings jsonSettings) { this.handlerFactory = container.GetInstance; this.jsonSettings = jsonSettings; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { HttpRequest request = context.Request; string commandName = GetCommandName(request); if (request.Method == "POST" && CommandTypes.ContainsKey(commandName)) { Type commandType = CommandTypes[commandName]; string commandData = request.Body.ReadToEnd(); Type handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType); this.ApplyHeaders(request); dynamic handler = this.handlerFactory.Invoke(handlerType); try { dynamic command = JsonConvert.DeserializeObject( string.IsNullOrWhiteSpace(commandData) ? "{}" : commandData, commandType, this.jsonSettings); handler.Handle(command); var result = new ObjectResult(null); await context.WriteResultAsync(result); } catch (Exception exception) { var response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(exception, context); if (response != null) { await context.WriteResultAsync(response); } else { throw; } } } else { await context.WriteResultAsync(new NotFoundObjectResult(commandName)); } } private void ApplyHeaders(HttpRequest request) { // TODO: Here you read the relevant headers and and check them or apply them to the current scope // so the values are accessible during execution of the command. string sessionId = request.Headers.GetValueOrNull("sessionId"); string token = request.Headers.GetValueOrNull("CSRF-token"); } private static string GetCommandName(HttpRequest request) { Uri requestUri = new Uri(request.GetEncodedUrl()); return requestUri.Segments.LastOrDefault(); } } } ================================================ FILE: src/WebCore3Service/Code/HeaderDictionaryExtensions.cs ================================================ namespace WebCoreService.Code { using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; public static class HeaderDictionaryExtensions { public static string GetValueOrNull(this IHeaderDictionary headers, string key) { if (!headers.TryGetValue(key, out StringValues value)) return null; return value[0]; } } } ================================================ FILE: src/WebCore3Service/Code/HttpContextExtensions.cs ================================================ namespace WebCoreService.Code { using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; // https://github.com/aspnet/Mvc/issues/7238#issuecomment-357391426 public static class HttpContextExtensions { private static readonly RouteData EmptyRouteData = new RouteData(); private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); public static Task WriteResultAsync(this HttpContext context, TResult result) where TResult : ObjectResult { if (context == null) throw new ArgumentNullException(nameof(context)); var executor = result is NotFoundObjectResult ? context.RequestServices.GetService>() : context.RequestServices.GetService>(); if (executor == null) throw new InvalidOperationException( $"No result executor for '{typeof(TResult).FullName}' has been registered."); var routeData = context.GetRouteData() ?? EmptyRouteData; var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor); return executor.ExecuteAsync(actionContext, result); } } } ================================================ FILE: src/WebCore3Service/Code/QueryHandlerMiddleware.cs ================================================ namespace WebCoreService.Code { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Contract; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using SimpleInjector; public sealed class QueryHandlerMiddleware : IMiddleware { private static readonly Dictionary QueryTypes; private readonly Func handlerFactory; private readonly JsonSerializerSettings jsonSettings; static QueryHandlerMiddleware() { QueryTypes = Bootstrapper.GetKnownQueryTypes().ToDictionary( info => info.QueryType.ToFriendlyName(), info => info, StringComparer.OrdinalIgnoreCase); } public QueryHandlerMiddleware(Container container, JsonSerializerSettings jsonSettings) { this.handlerFactory = container.GetInstance; this.jsonSettings = jsonSettings; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { HttpRequest request = context.Request; string queryName = GetQueryName(request); if (QueryTypes.ContainsKey(queryName)) { // GET operations get their data through the query string, while POST operations expect a JSON // object being put in the body. string queryData = request.Method.Equals("get", StringComparison.OrdinalIgnoreCase) ? SerializationHelpers.ConvertQueryStringToJson(request.QueryString.Value) : request.Body.ReadToEnd(); var (queryType, resultType) = QueryTypes[queryName]; Type handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryType, resultType); this.ApplyHeaders(request); dynamic handler = this.handlerFactory.Invoke(handlerType); try { dynamic query = JsonConvert.DeserializeObject( string.IsNullOrWhiteSpace(queryData) ? "{}" : queryData, queryType, this.jsonSettings); object result = handler.Handle(query); await context.WriteResultAsync(new ObjectResult(result)); } catch (Exception exception) { ObjectResult response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(exception, context); if (response != null) { await context.WriteResultAsync(response); } else { throw; } } } else { var response = new ObjectResult(queryName) { StatusCode = StatusCodes.Status404NotFound }; await context.WriteResultAsync(response); } } private void ApplyHeaders(HttpRequest request) { // TODO: Here you read the relevant headers and check them or apply them to the current scope // so the values are accessible during execution of the query. string sessionId = request.Headers.GetValueOrNull("sessionId"); string token = request.Headers.GetValueOrNull("CSRF-token"); } private static string GetQueryName(HttpRequest request) { Uri requestUri = new Uri(request.GetEncodedUrl()); return requestUri.Segments.LastOrDefault(); } } } ================================================ FILE: src/WebCore3Service/Code/SerializationHelpers.cs ================================================ namespace WebCoreService.Code { using System.Collections.Generic; using System.Linq; using System.Web; public static class SerializationHelpers { // NOTE: arrays and dictionary formats are not supported. e.g. this won't work: // ?values[0]=a&values[1]=b&x[A]=1&x[B]=5 public static string ConvertQueryStringToJson(string query) { var collection = HttpUtility.ParseQueryString(query); var dictionary = collection.AllKeys.ToDictionary(key => key, key => collection[key]); return ConvertDictionaryToJson(dictionary); } private static string ConvertDictionaryToJson(Dictionary dictionary) { var propertyNames = from key in dictionary.Keys let index = key.IndexOf(value: '.') select index < 0 ? key : key.Substring(0, index); var data = from propertyName in propertyNames.Distinct() let json = dictionary.ContainsKey(propertyName) ? HttpUtility.JavaScriptStringEncode(dictionary[propertyName], addDoubleQuotes: true) : ConvertDictionaryToJson(FilterByPropertyName(dictionary, propertyName)) select HttpUtility.JavaScriptStringEncode(propertyName, addDoubleQuotes: true) + ": " + json; return "{ " + string.Join(", ", data) + " }"; } private static Dictionary FilterByPropertyName(Dictionary dictionary, string propertyName) { string prefix = propertyName + "."; return dictionary.Keys .Where(key => key.StartsWith(prefix)) .ToDictionary(key => key.Substring(prefix.Length), key => dictionary[key]); } } } ================================================ FILE: src/WebCore3Service/Code/StreamExtensions.cs ================================================ namespace WebCoreService.Code { using System.IO; public static class StreamExtensions { public static string ReadToEnd(this Stream stream) { string result; using (var reader = new StreamReader(stream)) { result = reader.ReadToEnd(); } return result; } } } ================================================ FILE: src/WebCore3Service/Code/WebApiErrorResponseBuilder.cs ================================================ namespace WebCoreService.Code { using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Net; using System.Security; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Newtonsoft.Json; public static class WebApiErrorResponseBuilder { // Allows translating exceptions thrown by the business layer to HttpResponseExceptions. // This allows returning useful error information to the client. public static ObjectResult CreateErrorResponseOrNull(Exception thrownException, HttpContext context) { // TODO: context.Response.ContentType is always null, not sure why. var contentTypes = new MediaTypeCollection { context.Response.ContentType ?? "application/json" }; // Here are some examples of how certain exceptions can be mapped to error responses. switch (thrownException) { case JsonException _: // Return when the supplied model (command or query) can't be deserialized. return new BadRequestObjectResult(thrownException.Message) { ContentTypes = contentTypes }; case ValidationException exception: // Return when the supplied model (command or query) isn't valid. return new BadRequestObjectResult(exception.ValidationResult) { ContentTypes = contentTypes }; // case OptimisticConcurrencyException _: // // Return when there was a concurrency conflict in updating the model. // return new ConflictObjectResult(thrownException.Message) { ContentTypes = contentTypes }; case SecurityException _: // Return when the current user doesn't have the proper rights to execute the requested // operation or to access the requested resource. return new ObjectResult(null) { ContentTypes = contentTypes, StatusCode = (int)HttpStatusCode.Unauthorized }; case KeyNotFoundException _: // Return when the requested resource does not exist anymore. Catching a KeyNotFoundException // is an example, but you probably shouldn't throw KeyNotFoundException in this case, since it // could be thrown for other reasons (such as program errors) in which case this branch should // of course not execute. return new NotFoundObjectResult(thrownException.Message) { ContentTypes = contentTypes }; } // If the thrown exception can't be handled: return null. return null; } } } ================================================ FILE: src/WebCore3Service/Program.cs ================================================ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace WebCoreService { // See the Startup class for two examples of query urls. public static class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup(); } } ================================================ FILE: src/WebCore3Service/Properties/launchSettings.json ================================================ { "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:49228", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WebCoreService": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/values", "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/WebCore3Service/Startup.cs ================================================ using WebCoreService.Code; namespace WebCoreService { using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using SimpleInjector; // NOTE: Here are two example urls for queries: // * http://localhost:49228/api/queries/GetUnshippedOrdersForCurrentCustomer?Paging.PageIndex=3&Paging.PageSize=10 // * http://localhost:49228/api/queries/GetOrderById?OrderId=97fc6660-283d-44b6-b170-7db0c2e2afae public class Startup { private readonly Container container = new Container(); private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), #if DEBUG Formatting = Formatting.Indented, #endif }; public Startup(IConfiguration configuration) { container.Options.ResolveUnregisteredConcreteTypes = false; Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // We need to—at least—call AddMvcCore(), because it registers 11 implementations of the // IActionResultExecutor interface. Those are required by the HttpContextExtensions // method. Ideally, the use of MVC should not be required at all, which can considerably lower // the deployment footprint, but we're not there yet. Feedback is welcome. services .AddMvcCore() .AddNewtonsoftJson(); services.AddSimpleInjector(this.container, options => { options.AddAspNetCore(); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.ApplicationServices.UseSimpleInjector(this.container); Bootstrapper.Bootstrap(this.container); // Map routes to the middleware for query handling and command handling app.Map("/api/queries", b => UseMiddleware(b, new QueryHandlerMiddleware(this.container, JsonSettings))); app.Map("/api/commands", b => UseMiddleware(b, new CommandHandlerMiddleware(this.container, JsonSettings))); this.container.Verify(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } } private static void UseMiddleware(IApplicationBuilder app, IMiddleware middleware) => app.Use((c, next) => middleware.InvokeAsync(c, _ => next())); } } ================================================ FILE: src/WebCore3Service/WebCore3Service.csproj ================================================ netcoreapp3.1 WebCoreService ================================================ FILE: src/WebCore3Service/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ================================================ FILE: src/WebCore3Service/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: src/WebCore6Service/Bootstrapper.cs ================================================ namespace WebCoreService; using BusinessLayer; using SimpleInjector; using System.Diagnostics; using System.Security.Principal; public static class Bootstrapper { public static IEnumerable GetKnownCommandTypes() => BusinessLayerBootstrapper.GetCommandTypes(); public static IEnumerable<(Type QueryType, Type ResultType)> GetKnownQueryTypes() => BusinessLayerBootstrapper.GetQueryTypes(); public static Container Bootstrap(Container container) { BusinessLayerBootstrapper.Bootstrap(container); container.RegisterSingleton(); container.RegisterInstance(new DebugLogger()); return container; } private sealed class HttpContextPrincipal : IPrincipal { private readonly IHttpContextAccessor httpContextAccessor; public HttpContextPrincipal(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public IIdentity Identity => this.Principal.Identity!; public bool IsInRole(string role) => this.Principal.IsInRole(role); private IPrincipal Principal => this.httpContextAccessor.HttpContext?.User!; } private sealed class DebugLogger : BusinessLayer.ILogger { public void Log(string message) { Debug.WriteLine(message); } } } ================================================ FILE: src/WebCore6Service/Code/Commands.cs ================================================ namespace WebCoreService; using BusinessLayer; using Contract; using SimpleInjector; // This class is named "Commands" to allow Swagger to group command handler routes. public sealed record Commands(Container Container) { public Task InvokeAsync(TCommand command) where TCommand : ICommand { var handler = Container.GetInstance>(); try { handler.Handle(command); return Task.FromResult(Results.Ok()); } catch (Exception exception) { var response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(exception); if (response != null) { return Task.FromResult(response); } else { throw; } } } } ================================================ FILE: src/WebCore6Service/Code/FlatApiMessageMappingBuilder.cs ================================================ namespace WebCoreService; using System.Reflection; /// /// Builds mappings based on a flat model where: /// * routes are presented as a non-hierarchical (flat) list, e.g. /// /api/ShipOrder, /api/CancelOrder, /api/GetAllOrders, /api/FormatHardDrive, etc. /// * only the POST verb is used (i.e. messages are always sent through HTTP Post operations). /// This is ideal for .NET clients that reuse the assembly (the Contract project), and are not interested in /// having a rich REST-full API at their disposal. /// public sealed class FlatApiMessageMappingBuilder : IMessageMappingBuilder { private readonly string patternFormat; private readonly object dispatcher; private readonly MethodInfo genericMethod; public FlatApiMessageMappingBuilder(object dispatcher, string patternFormat = "/api/{0}") { this.patternFormat = patternFormat; this.dispatcher = dispatcher; this.genericMethod = dispatcher.GetType().GetMethod("InvokeAsync") ?? throw new ArgumentException("InvokeAsync method is missing."); } public (string, string[], Delegate) BuildMapping(Type messageType, Type? returnType) { (Type[] GenericArguments, Type GenericFuncType) args = returnType != null ? (new[] { messageType, returnType }, typeof(Func<,,>)) : (new[] { messageType }, typeof(Func<,>)); MethodInfo method = this.genericMethod.MakeGenericMethod(args.GenericArguments); Type funcType = args.GenericFuncType.MakeGenericType( method.GetParameters().Append(method.ReturnParameter).Select(p => p.ParameterType).ToArray()); Delegate handler = Delegate.CreateDelegate(funcType, dispatcher, method); var pattern = string.Format(this.patternFormat, GetMessageRoute(messageType)); // Hi, dear reader. I need your help. This method registers a query call as a HTTP POST action. // This might be fine for some APIs, others might require the query object to be called as HTTP // GET, while its arguments are serialized as part of the URL query string. This isn't supported // at the moment. To give an example, using the POST operation, the data for the // GetUnshippedOrdersForCurrentCustomerQuery query is serialized in the HTTP body, as the // { Paging { PageIndex = 3, PageSize = 10 } } JSON string. Using GET and the query string // instead, the request could look as follows: // /api/queries/GetUnshippedOrdersForCurrentCustomer?Paging.PageIndex=3&Paging.PageSize=10. // The WebCoreService project actually contains a SerializationHelpers that allows deserializing // a query string back to a DTO, but there isn't any support for Swagger in there. At this point, // it's unclear to me how to achieve this using the new ASP.NET Core Minimal API, while // 1. (preferably) keeping the implementation simple, and // 2. allowing this without the need for any query-specific code, and // 3. allowing this to integrate nicely in the Swagger and API Explorer. // If you have any suggestions, you can send me a pull request, or start a conversation here: // https://github.com/dotnetjunkie/solidservices/issues/new. return (pattern, new[] { HttpMethods.Post }, handler); } private static string GetMessageRoute(Type messageType) => // ToFriendlyName builds an easy to read type name. Namespaces will be omitted, and generic types // will be displayed in a C#-like syntax. SimpleInjector.TypesExtensions.ToFriendlyName(messageType) // Replace generic markers. Typically they are allowed as root, but that would be frowned upon. .Replace("<", string.Empty).Replace(">", string.Empty); } ================================================ FILE: src/WebCore6Service/Code/IMessageMappingBuilder.cs ================================================ namespace WebCoreService; public interface IMessageMappingBuilder { (string Pattern, string[] HttpMethods, Delegate Handler) BuildMapping(Type messageType, Type? returnType = null); } ================================================ FILE: src/WebCore6Service/Code/MessageMapping.cs ================================================ namespace WebCoreService; public static class MessageMapping { public static IMessageMappingBuilder FlatApi(object dispatcher, string patternFormat = "/api/{0}") => new FlatApiMessageMappingBuilder(dispatcher, patternFormat); } ================================================ FILE: src/WebCore6Service/Code/MessageMappingExtensions.cs ================================================ namespace WebCoreService; public static class MessageMappingExtensions { public static void MapCommands( this IEndpointRouteBuilder app, IMessageMappingBuilder pattern, IEnumerable commandTypes) { foreach (Type commandType in commandTypes) { app.MapMessage(pattern, commandType); } } public static void MapQueries( this IEndpointRouteBuilder app, IMessageMappingBuilder pattern, IEnumerable<(Type QueryType, Type ResultType)> queryTypes) { foreach (var info in queryTypes) { app.MapMessage(pattern, info.QueryType, info.ResultType); } } public static void MapMessage( this IEndpointRouteBuilder app, IMessageMappingBuilder pattern, Type messageType, Type? returnType = null) { var mapping = pattern.BuildMapping(messageType, returnType); app.MapMethods(mapping.Pattern, mapping.HttpMethods, mapping.Handler); } } ================================================ FILE: src/WebCore6Service/Code/Queries.cs ================================================ namespace WebCoreService; using Contract; using SimpleInjector; // This class is named "Queries" to allow Swagger to group query handler routes. public sealed record Queries(Container Container) { public async Task InvokeAsync(HttpContext context, TQuery query) where TQuery : IQuery { var handler = Container.GetInstance>(); try { TResult result = handler.Handle(query); return result; } catch (Exception exception) { var response = WebApiErrorResponseBuilder.CreateErrorResponseOrNull(exception); if (response != null) { await response.ExecuteAsync(context); return default!; } else { throw; } } } } ================================================ FILE: src/WebCore6Service/Code/WebApiErrorResponseBuilder.cs ================================================ namespace WebCoreService; using Newtonsoft.Json; using System.ComponentModel.DataAnnotations; using System.Security; public static class WebApiErrorResponseBuilder { // Allows translating exceptions thrown by the business layer to HttpResponseExceptions. // This allows returning useful error information to the client. public static IResult? CreateErrorResponseOrNull(Exception thrownException) { // Here are some examples of how certain exceptions can be mapped to error responses. switch (thrownException) { case JsonException: // Return when the supplied model (command or query) can't be deserialized. return Results.BadRequest(thrownException.Message); case ValidationException exception: // Return when the supplied model (command or query) isn't valid. return Results.BadRequest(exception.ValidationResult); case SecurityException: // Return when the current user doesn't have the proper rights to execute the requested // operation or to access the requested resource. return Results.Unauthorized(); case KeyNotFoundException: // Return when the requested resource does not exist anymore. Catching a KeyNotFoundException // is an example, but you probably shouldn't throw KeyNotFoundException in this case, since it // could be thrown for other reasons (such as program errors) in which case this branch should // of course not execute. return Results.NotFound(thrownException.Message); default: // If the thrown exception can't be handled: return null. return null; } } } ================================================ FILE: src/WebCore6Service/Program.cs ================================================ using Microsoft.OpenApi.Models; using SimpleInjector; using WebCoreService; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Open http://localhost:5132/swagger/ to browse the API. services.AddEndpointsApiExplorer(); services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "SOLID Services API" }); // The XML comment files are copied using a post-build event (see project settings / Build Events). options.IncludeXmlDocumentationFromDirectory(AppDomain.CurrentDomain.BaseDirectory); // Optional but useful: this includes the summaries of the command and query types in the operations. options.IncludeMessageSummariesFromXmlDocs(AppDomain.CurrentDomain.BaseDirectory); }); var container = new Container(); services.AddSimpleInjector(container, options => { options.AddAspNetCore(); }); Bootstrapper.Bootstrap(container); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.MapCommands( pattern: MessageMapping.FlatApi(new Commands(container), "/api/commands/{0}"), commandTypes: Bootstrapper.GetKnownCommandTypes()); app.MapQueries( pattern: MessageMapping.FlatApi(new Queries(container), "/api/queries/{0}"), queryTypes: Bootstrapper.GetKnownQueryTypes()); app.Run(); ================================================ FILE: src/WebCore6Service/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:29597", "sslPort": 0 } }, "profiles": { "WebCoreMinimalApiService": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "http://localhost:5132", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: src/WebCore6Service/Swagger/SwaggerExtensions.cs ================================================ namespace WebCoreService; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; public static class SwaggerExtensions { public static void IncludeXmlDocumentationFromDirectory(this SwaggerGenOptions options, string appDataPath) { string[] xmlCommentsPaths = Directory.GetFiles(appDataPath, "*.xml"); if (!xmlCommentsPaths.Any()) { throw new InvalidOperationException("No .xml files were found in the App_Data folder."); } foreach (string xmlCommentsPath in xmlCommentsPaths) { options.IncludeXmlComments(xmlCommentsPath); } } // The query and command types are the operations, but Swagger nor Web API knows this. This method extracts the // summary from class (if available) and places it on the operation. public static void IncludeMessageSummariesFromXmlDocs(this SwaggerGenOptions options, string appDataPath) { string[] xmlCommentsPaths = Directory.GetFiles(appDataPath, "*.xml"); options.OperationFilter(new object[] { xmlCommentsPaths }); } public sealed class AddMessageSummaryOperationFilter : IOperationFilter { private readonly XmlDocumentationTypeDescriptionProvider[] providers; public AddMessageSummaryOperationFilter(string[] xmlCommentsPaths) { this.providers = xmlCommentsPaths.Select(p => new XmlDocumentationTypeDescriptionProvider(p)).ToArray(); } public void Apply(OpenApiOperation operation, OperationFilterContext context) { var api = context.ApiDescription; var type = context.ApiDescription.ParameterDescriptions.LastOrDefault()?.Type; if (type != null) { operation.Summary = this.GetSummaries(type).FirstOrDefault() ?? operation.Summary; } } private IEnumerable GetSummaries(Type type) => from provider in providers let description = provider.GetDescription(type) where description != null select description; } } ================================================ FILE: src/WebCore6Service/Swagger/XmlDocumentationTypeDescriptionProvider.cs ================================================ namespace WebCoreService; using System.Globalization; using System.Xml.XPath; // NOTE: The code in this file is copy-pasted from the default Web API Visual Studio 2013 template. /// /// Allows getting type descriptions based on .NET XML documentation files that are generated by the /// C# or VB compiler. /// public sealed class XmlDocumentationTypeDescriptionProvider { private const string TypeExpression = "/doc/members/member[@name='T:{0}']"; private XPathNavigator _documentNavigator; /// /// Initializes a new instance of the class. /// /// The physical path to XML document. public XmlDocumentationTypeDescriptionProvider(string documentPath) { if (documentPath is null) throw new ArgumentNullException(nameof(documentPath)); _documentNavigator = new XPathDocument(documentPath).CreateNavigator(); } /// Gets the type's description or null when there is no description for the given type. /// The type. /// The description of the requested type or null. public string? GetDescription(Type type) { XPathNavigator typeNode = GetTypeNode(type); return GetTagValue(typeNode, "summary"); } private XPathNavigator GetTypeNode(Type type) { string controllerTypeName = GetTypeName(type); string selectExpression = String.Format(CultureInfo.InvariantCulture, TypeExpression, controllerTypeName); return _documentNavigator.SelectSingleNode(selectExpression)!; } private static string? GetTagValue(XPathNavigator parentNode, string tagName) { if (parentNode != null) { XPathNavigator? node = parentNode.SelectSingleNode(tagName); if (node != null) { return node.Value.Trim(); } } return null; } private static string GetTypeName(Type type) { string name = type.FullName!; if (type.IsGenericType) { // Format the generic type name to something like: Generic{System.Int32,System.String} Type genericType = type.GetGenericTypeDefinition(); Type[] genericArguments = type.GetGenericArguments(); string genericTypeName = genericType.FullName!; // Trim the generic parameter counts from the name genericTypeName = genericTypeName.Substring(0, genericTypeName.IndexOf('`')); string[] argumentTypeNames = genericArguments.Select(t => GetTypeName(t)).ToArray(); name = String.Format(CultureInfo.InvariantCulture, "{0}{{{1}}}", genericTypeName, String.Join(",", argumentTypeNames)); } if (type.IsNested) { // Changing the nested type name from OuterType+InnerType to OuterType.InnerType to match the XML documentation syntax. name = name.Replace("+", "."); } return name; } } ================================================ FILE: src/WebCore6Service/WebCore6Service.csproj ================================================ net6.0 enable enable WebCoreService ================================================ FILE: src/WebCore6Service/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: src/WebCore6Service/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }