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