Repository: dotnetjunkie/solidservices
Branch: master
Commit: 2d7c3de74877
Files: 131
Total size: 201.7 KB
Directory structure:
gitextract_3fjw8qzp/
├── .gitignore
├── LICENSE
├── README.md
└── src/
├── .gitignore
├── BusinessLayer/
│ ├── BusinessLayer.csproj
│ ├── BusinessLayerBootstrapper.cs
│ ├── CommandHandlers/
│ │ ├── CreateOrderCommandHandler.cs
│ │ └── ShipOrderCommandHandler.cs
│ ├── CrossCuttingConcerns/
│ │ ├── AuthorizationCommandHandlerDecorator.cs
│ │ ├── AuthorizationQueryHandlerDecorator.cs
│ │ ├── DataAnnotationsValidator.cs
│ │ ├── StructuredLoggingCommandHandlerDecorator.cs
│ │ ├── StructuredLoggingQueryHandlerDecorator.cs
│ │ ├── StructuredMessageLogger.cs
│ │ ├── ValidationCommandHandlerDecorator.cs
│ │ └── ValidationQueryHandlerDecorator.cs
│ ├── Helpers/
│ │ └── PagingExtensions.cs
│ ├── ICommandHandler.cs
│ ├── ILogger.cs
│ ├── IQueryHandler.cs
│ ├── IValidator.cs
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ ├── QueryHandlers/
│ │ ├── GetOrderByIdQueryHandler.cs
│ │ └── GetUnshippedOrdersQueryHandler.cs
│ └── packages.config
├── Client/
│ ├── App.config
│ ├── Bootstrapper.cs
│ ├── Client.csproj
│ ├── Code/
│ │ ├── CommandServiceClient.cs
│ │ ├── DynamicQueryProcessor.cs
│ │ ├── QueryServiceClient.cs
│ │ ├── WcfServiceCommandHandlerProxy.cs
│ │ └── WcfServiceQueryHandlerProxy.cs
│ ├── Controllers/
│ │ ├── CommandExampleController.cs
│ │ └── QueryExampleController.cs
│ ├── CrossCuttingConcerns/
│ │ └── FromWcfFaultTranslatorCommandHandlerDecorator.cs
│ ├── ICommandHandler.cs
│ ├── IQueryHandler.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ ├── Wcf/
│ │ ├── KnownCommandTypesAttribute.cs
│ │ ├── KnownQueryAndResultTypesAttribute.cs
│ │ ├── KnownTypesAttribute.cs
│ │ └── KnownTypesDataContractResolver.cs
│ └── packages.config
├── Contract/
│ ├── Commands/
│ │ └── Orders/
│ │ ├── CreateOrder.cs
│ │ └── ShipOrder.cs
│ ├── Contract.csproj
│ ├── DTOs/
│ │ ├── Address.cs
│ │ └── OrderInfo.cs
│ ├── ICommand.cs
│ ├── IQuery.cs
│ ├── IQueryProcessor.cs
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ ├── Queries/
│ │ ├── Orders/
│ │ │ ├── GetOrderById.cs
│ │ │ └── GetUnshippedOrders.cs
│ │ ├── PageInfo.cs
│ │ └── Paged.cs
│ └── Validators/
│ ├── CompositeValidationResult.cs
│ ├── NonEmptyGuidAttribute.cs
│ └── ValidateObjectAttribute.cs
├── Settings.StyleCop
├── SolidServices.sln
├── WcfService/
│ ├── Bootstrapper.cs
│ ├── Code/
│ │ ├── DebugLogger.cs
│ │ └── WcfExceptionTranslator.cs
│ ├── CommandService.svc
│ ├── CommandService.svc.cs
│ ├── CrossCuttingConcerns/
│ │ └── ToWcfFaultTranslatorCommandHandlerDecorator.cs
│ ├── Global.asax
│ ├── Global.asax.cs
│ ├── NonDotNetQueryService.cs
│ ├── NonDotNetQueryService.svc
│ ├── NonDotNetQueryService.tt
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ ├── QueryService.svc
│ ├── QueryService.svc.cs
│ ├── ValidationError.cs
│ ├── WcfService.csproj
│ ├── Web.Debug.config
│ ├── Web.Release.config
│ ├── Web.config
│ └── packages.config
├── WebApiService/
│ ├── App_Data/
│ │ └── .gitignore
│ ├── App_Start/
│ │ ├── FilterConfig.cs
│ │ ├── RouteConfig.cs
│ │ ├── SwaggerConfig.cs
│ │ └── WebApiConfig.cs
│ ├── Bootstrapper.cs
│ ├── Code/
│ │ ├── CommandDelegatingHandler.cs
│ │ ├── ExampleObjectCreator.cs
│ │ ├── QueryDelegatingHandler.cs
│ │ ├── SerializationHelpers.cs
│ │ └── WebApiExceptionTranslator.cs
│ ├── Global.asax
│ ├── Global.asax.cs
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ ├── Web.Debug.config
│ ├── Web.Release.config
│ ├── Web.config
│ ├── WebApiService.csproj
│ └── packages.config
├── WebCore3Service/
│ ├── Bootstrapper.cs
│ ├── Code/
│ │ ├── CommandHandlerMiddleware.cs
│ │ ├── HeaderDictionaryExtensions.cs
│ │ ├── HttpContextExtensions.cs
│ │ ├── QueryHandlerMiddleware.cs
│ │ ├── SerializationHelpers.cs
│ │ ├── StreamExtensions.cs
│ │ └── WebApiErrorResponseBuilder.cs
│ ├── Program.cs
│ ├── Properties/
│ │ └── launchSettings.json
│ ├── Startup.cs
│ ├── WebCore3Service.csproj
│ ├── appsettings.Development.json
│ └── appsettings.json
└── WebCore6Service/
├── Bootstrapper.cs
├── Code/
│ ├── Commands.cs
│ ├── FlatApiMessageMappingBuilder.cs
│ ├── IMessageMappingBuilder.cs
│ ├── MessageMapping.cs
│ ├── MessageMappingExtensions.cs
│ ├── Queries.cs
│ └── WebApiErrorResponseBuilder.cs
├── Program.cs
├── Properties/
│ └── launchSettings.json
├── Swagger/
│ ├── SwaggerExtensions.cs
│ └── XmlDocumentationTypeDescriptionProvider.cs
├── WebCore6Service.csproj
├── appsettings.Development.json
└── appsettings.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
/.vs
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2019 Steven van Deursen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Highly Maintainable Web Services
The Highly Maintainable Web Services project is a reference architecture application for .NET that demonstrates how to build highly maintainable web services.
This project contains no documentation, just code. Please visit the following article for the reasoning behind this project:
* [Writing Highly Maintainable WCF services](https://blogs.cuttingedge.it/steven/posts/2012/writing-highly-maintainable-wcf-services/)
For more background about the used design, please read the following articles:
* [Meanwhile… on the command side of my architecture](https://blogs.cuttingedge.it/steven/p/commands/)
* [Meanwhile… on the query side of my architecture](https://blogs.cuttingedge.it/steven/p/queries/)
This project contains the following Web Service projects that all expose the same set of commands and queries:
* [WCF](https://github.com/dotnetjunkie/solidservices/tree/master/src/WcfService). This project exposes command and query messages through a WCF SOAP service, while all messages are specified explicitly through a WSDL. This exposes an explicit contract to the client, although serialization and deserialization of messages is quite limited in WCF, which likely causes problems when sending and receiving messages, unless the messages are explicitly designed with the WCF-serialization constraints in mind. The [Client](https://github.com/dotnetjunkie/solidservices/tree/master/src/Client) project sends queries and commands through the WCF Service. Due to the setup, it gives full integration into the WCF pipeline, which includes security, logging, encryption, and authorization.
* [ASP.NET 'Classic' 4.8 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebApiService) (includes a Swagger API). This project exposes commands and queries as REST API through the System.Web.Http stack (the 'legacy' ASP.NET Web API) of .NET 4.8. REST makes the contract less explicit, but allows more flexibility over a SOAP service. It uses JSON.NET as serialization mechanism, which allows much flexibility in defining command and query messages. Incoming requests are mapped to HttpMessageHandlers, which dispatch messages to underlying command and query handlers. In doing so, it circumvents a lot of the Web API infrastructure, which means logging and security might need to be dealt with separately. This project uses an external NuGet library to allow exposing its API through an OpenAPI/Swagger interface.
* [ASP.NET Core 3.1 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebCore3Service). This project exposes commands and queries as REST API through ASP.NET Core 3.1's Web API. REST makes the contract less explicit but allows more flexibility over a SOAP service. Just as the previous 'classic' Web API project, it uses JSON.NET as serialization mechanism, which allows much flexibility in defining command and query messages. Incoming requests are mapped to specific Middleware, which dispatches messages to underlying command and query handlers. In doing so, it circumvents a lot of the ASP.NET Core Web API infrastructure, which means logging and security might need to be dealt with separately. This project has _no_ support for exposing its API through OpenAPI/Swagger.
* [ASP.NET Core 6 Web API](https://github.com/dotnetjunkie/solidservices/tree/master/src/WebCore6Service) (includes a Swagger API). This project exposes commands and queries as REST API through ASP.NET Core 6 Minimal API. The project uses .NET 6's System.Text.Json as serialization mechanism, which is the built-in mechanism. It is less flexible compared to JSON.NET, but gives superb performance. This project makes full use of the new Minimal API functionality and maps each query and command to a specific URL. This allows full integration into the ASP.NET Core pipeline, including logging, security, and OpenAPI/Swagger. There is some extra code added to expose command and query XML documentation summaries through as part of the operation's documentation. Due to limitations in the Minimal API framework, queries only support HTTP POST operations.
================================================
FILE: src/.gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Security
*.snk
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
Help/
*.boltdata/
# Visual Studo 2015 cache/options directory
.vs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
*.[Cc]ache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
================================================
FILE: src/BusinessLayer/BusinessLayer.csproj
================================================
Debug
AnyCPU
8.0.30703
2.0
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}
Library
Properties
BusinessLayer
BusinessLayer
v4.8
512
SAK
SAK
SAK
SAK
true
full
false
bin\Debug\
DEBUG;TRACE
prompt
4
true
false
pdbonly
true
bin\Release\
TRACE
prompt
4
true
false
..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll
..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll
..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll
..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll
{DDD88351-9A73-4212-85DC-F769B37D5057}
Contract
================================================
FILE: src/BusinessLayer/BusinessLayerBootstrapper.cs
================================================
namespace BusinessLayer
{
using BusinessLayer.CrossCuttingConcerns;
using Contract;
using SimpleInjector;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
// This class allows registering all types that are defined in the business layer, and are shared across
// all applications that use this layer (WCF and Web API). For simplicity, this class is placed inside
// this assembly, but this does couple the business layer assembly to the used container. If this is a
// concern, create a specific BusinessLayer.Bootstrap project with this class.
public static class BusinessLayerBootstrapper
{
private static readonly Assembly[] ContractAssemblies = new[] { typeof(IQuery<>).Assembly };
private static readonly Assembly[] BusinessLayerAssemblies = new[] { Assembly.GetExecutingAssembly() };
public static void Bootstrap(Container container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
container.RegisterInstance(new DataAnnotationsValidator());
container.Register(typeof(ICommandHandler<>), BusinessLayerAssemblies);
container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
container.RegisterDecorator(typeof(ICommandHandler<>), typeof(AuthorizationCommandHandlerDecorator<>));
container.Register(typeof(IQueryHandler<,>), BusinessLayerAssemblies);
container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(ValidationQueryHandlerDecorator<,>));
container.RegisterDecorator(typeof(IQueryHandler<,>), typeof(AuthorizationQueryHandlerDecorator<,>));
}
public static IEnumerable GetCommandTypes() =>
from assembly in ContractAssemblies
from type in assembly.GetExportedTypes()
where typeof(ICommand).IsAssignableFrom(type)
where !type.IsAbstract
select type;
public static Type CreateQueryHandlerType(Type queryType) =>
typeof(IQueryHandler<,>).MakeGenericType(queryType, DetermineResultTypes(queryType).Single());
public static IEnumerable<(Type QueryType, Type ResultType)> GetQueryTypes() =>
from assembly in ContractAssemblies
from type in assembly.GetExportedTypes()
where IsQuery(type)
select (type, DetermineResultTypes(type).Single());
public static Type GetQueryResultType(Type queryType) => DetermineResultTypes(queryType).Single();
private static bool IsQuery(Type type) => DetermineResultTypes(type).Any();
private static IEnumerable DetermineResultTypes(Type type) =>
from interfaceType in type.GetInterfaces()
where interfaceType.IsGenericType
where interfaceType.GetGenericTypeDefinition() == typeof(IQuery<>)
select interfaceType.GetGenericArguments()[0];
}
}
================================================
FILE: src/BusinessLayer/CommandHandlers/CreateOrderCommandHandler.cs
================================================
namespace BusinessLayer.CommandHandlers
{
using Contract;
using Contract.Commands.Orders;
public class CreateOrderCommandHandler : ICommandHandler
{
private readonly ILogger logger;
public CreateOrderCommandHandler(ILogger logger)
{
this.logger = logger;
}
public void Handle(CreateOrder command)
{
// Do something useful here.
this.logger.Log(this.GetType().Name + " has been executed successfully.");
}
}
}
================================================
FILE: src/BusinessLayer/CommandHandlers/ShipOrderCommandHandler.cs
================================================
namespace BusinessLayer.CommandHandlers
{
using Contract;
using Contract.Commands.Orders;
public class ShipOrderCommandHandler : ICommandHandler
{
private readonly ILogger logger;
public ShipOrderCommandHandler(ILogger logger)
{
this.logger = logger;
}
public void Handle(ShipOrder command)
{
this.logger.Log("Shipping order " + command.OrderId);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/AuthorizationCommandHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using System.Security;
using System.Security.Principal;
using Contract;
public class AuthorizationCommandHandlerDecorator : ICommandHandler where TCommand : ICommand
{
private readonly ICommandHandler decoratedHandler;
private readonly IPrincipal currentUser;
private readonly ILogger logger;
public AuthorizationCommandHandlerDecorator(ICommandHandler decoratedHandler,
IPrincipal currentUser, ILogger logger)
{
this.decoratedHandler = decoratedHandler;
this.currentUser = currentUser;
this.logger = logger;
}
public void Handle(TCommand query)
{
this.Authorize();
this.decoratedHandler.Handle(query);
}
private void Authorize()
{
// Some useful authorization logic here.
if (typeof(TCommand).Namespace.Contains("Admin") && !this.currentUser.IsInRole("Admin"))
{
throw new SecurityException();
}
this.logger.Log("User " + this.currentUser.Identity.Name + " has been authorized to execute " +
typeof(TCommand).Name);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/AuthorizationQueryHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using System.Security;
using System.Security.Principal;
using Contract;
public class AuthorizationQueryHandlerDecorator : IQueryHandler
where TQuery : IQuery
{
private readonly IQueryHandler decoratedHandler;
private readonly IPrincipal currentUser;
private readonly ILogger logger;
public AuthorizationQueryHandlerDecorator(IQueryHandler decoratedHandler,
IPrincipal currentUser, ILogger logger)
{
this.decoratedHandler = decoratedHandler;
this.currentUser = currentUser;
this.logger = logger;
}
public TResult Handle(TQuery query)
{
this.Authorize();
return this.decoratedHandler.Handle(query);
}
private void Authorize()
{
// Some useful authorization logic here.
if (typeof(TQuery).Namespace.Contains("Admin") && !this.currentUser.IsInRole("Admin"))
{
throw new SecurityException();
}
this.logger.Log("User " + this.currentUser.Identity.Name + " has been authorized to execute " +
typeof(TQuery).Name);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/DataAnnotationsValidator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
public class DataAnnotationsValidator : IValidator
{
[DebuggerStepThrough]
void IValidator.ValidateObject(object instance)
{
var context = new ValidationContext(instance, null, null);
// Throws an exception when instance is invalid.
Validator.ValidateObject(instance, context, validateAllProperties: true);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredLoggingCommandHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using Contract;
using System.Diagnostics;
public sealed class StructuredLoggingCommandHandlerDecorator : ICommandHandler
where TCommand : ICommand
{
private readonly StructuredMessageLogger logger;
private readonly ICommandHandler decoratee;
public StructuredLoggingCommandHandlerDecorator(
StructuredMessageLogger logger, ICommandHandler decoratee)
{
this.logger = logger;
this.decoratee = decoratee;
}
public void Handle(TCommand command)
{
var watch = Stopwatch.StartNew();
this.decoratee.Handle(command);
this.logger.Log(command, watch.Elapsed);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredLoggingQueryHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using Contract;
using System.Diagnostics;
public sealed class StructuredLoggingQueryHandlerDecorator
: IQueryHandler where TQuery : IQuery
{
private readonly StructuredMessageLogger logger;
private readonly IQueryHandler decoratee;
public StructuredLoggingQueryHandlerDecorator(
StructuredMessageLogger logger, IQueryHandler decoratee)
{
this.logger = logger;
this.decoratee = decoratee;
}
public TResult Handle(TQuery query)
{
var watch = Stopwatch.StartNew();
var result = this.decoratee.Handle(query);
this.logger.Log(query, watch.Elapsed);
return result;
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/StructuredMessageLogger.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using System;
using System.Linq;
using System.Reflection;
///
/// Logs information about the succesfull execution of a given TMessage, where the used template is specific to
/// the TMessage type with its specified properties. For instance, it might log the following:
///
/// this.logger.LogInformation(
/// "{Message} executed in {Milliseconds} with parameters {OrderId}",
/// "GetOrderById", // <-- {Message}
/// stopwatch.ElapsedMilliseconds, // <-- {Milliseconds}
/// message.OrderId); // <-- {OrderId}
///
///
///
public sealed class StructuredMessageLogger
{
private static readonly string MessageName;
private static readonly PropertyInfo[] MessageProperties;
private static readonly string LogTemplate;
private static readonly Type[] SupportedPropertyTypes =
typeof(int).Assembly.GetExportedTypes().Where(t => t.IsPrimitive)
.Concat(new[] { typeof(string), typeof(Guid) })
.ToArray();
private readonly ILogger logger;
static StructuredMessageLogger()
{
// PERF: By using a static constructor, initialization is done just once.
MessageName = typeof(TMessage).Name;
MessageProperties = GetLoggableMessageProperties();
LogTemplate = "{Message} executed in {Milliseconds}";
if (MessageProperties.Length > 0)
{
LogTemplate +=
" with parameters " +
string.Join(", ", MessageProperties.Select(prop => "{" + prop.Name + "}"));
}
}
public StructuredMessageLogger(ILogger logger)
{
this.logger = logger;
}
public void Log(TMessage message, TimeSpan elapsed)
{
object[] parameters = this.BuildParameters(message, elapsed);
this.logger.LogInformation(LogTemplate, parameters);
}
private object[] BuildParameters(TMessage message, TimeSpan elapsed)
{
var parameters = new object[MessageProperties.Length + 2];
parameters[0] = MessageName;
parameters[1] = (long)elapsed.TotalMilliseconds;
for (int i = 0; i < MessageProperties.Length; i++)
{
PropertyInfo property = MessageProperties[i];
// PERF: PropertyInfo.GetValue is pretty slow. If needed this can be optimized by compiling Expression
// trees.
parameters[i + 2] = property.GetValue(message);
}
return parameters;
}
private static PropertyInfo[] GetLoggableMessageProperties()
{
// TODO: Filter out unwanted properties (e.g. complex one or one's with sensitive info). You
// can do this based on an attribute that you place on the property or only include properties
// of certain primitive types (or both). The example here uses a white list of supported types
return (
from prop in typeof(TMessage).GetProperties(BindingFlags.Instance | BindingFlags.Public)
where SupportedPropertyTypes.Contains(prop.PropertyType)
orderby prop.Name // Sorting is important, because ordering is not guaranteed across restarts
select prop)
.ToArray();
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/ValidationCommandHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using Contract;
using System;
public class ValidationCommandHandlerDecorator : ICommandHandler where TCommand : ICommand
{
private readonly IValidator validator;
private readonly ICommandHandler handler;
public ValidationCommandHandlerDecorator(IValidator validator, ICommandHandler handler)
{
this.validator = validator;
this.handler = handler;
}
void ICommandHandler.Handle(TCommand command)
{
if (command == null) throw new ArgumentNullException(nameof(command));
// validate the supplied command.
this.validator.ValidateObject(command);
// forward the (valid) command to the real command handler.
this.handler.Handle(command);
}
}
}
================================================
FILE: src/BusinessLayer/CrossCuttingConcerns/ValidationQueryHandlerDecorator.cs
================================================
namespace BusinessLayer.CrossCuttingConcerns
{
using System;
using Contract;
public class ValidationQueryHandlerDecorator : IQueryHandler
where TQuery : IQuery
{
private readonly IValidator validator;
private readonly IQueryHandler handler;
public ValidationQueryHandlerDecorator(IValidator validator, IQueryHandler handler)
{
this.validator = validator;
this.handler = handler;
}
TResult IQueryHandler.Handle(TQuery query)
{
if (query == null) throw new ArgumentNullException(nameof(query));
// validate the supplied command.
this.validator.ValidateObject(query);
// forward the (valid) command to the real command handler.
return this.handler.Handle(query);
}
}
}
================================================
FILE: src/BusinessLayer/Helpers/PagingExtensions.cs
================================================
namespace BusinessLayer
{
using System.Collections.Generic;
using System.Linq;
using Contract.Queries;
public static class PagingExtensions
{
/// Apply paging in memory, using LINQ to Objects.
/// The type of objects to enumerate.
/// The collection
/// The optional paging object. When null, the default paging values will be used.
/// A paged result.
public static Paged Page(this IEnumerable collection, PageInfo paging)
{
paging = paging ?? new PageInfo();
IEnumerable items = paging.IsSinglePage()
? collection
: collection.Skip(paging.PageIndex * paging.PageSize).Take(paging.PageSize);
return new Paged { Items = items.ToArray(), Paging = paging };
}
/// Apply paging using an efficient database query.
/// The type of objects to enumerate.
/// The collection
/// The optional paging object. When null, the default paging values will be used.
/// A paged result.
public static Paged Page(this IQueryable collection, PageInfo paging)
{
paging = paging ?? new PageInfo();
IQueryable items = paging.IsSinglePage()
? collection
: collection.Skip(paging.PageIndex * paging.PageSize).Take(paging.PageSize);
return new Paged { Items = items.ToArray(), Paging = paging };
}
}
}
================================================
FILE: src/BusinessLayer/ICommandHandler.cs
================================================
using Contract;
namespace BusinessLayer
{
public interface ICommandHandler where TCommand : ICommand
{
void Handle(TCommand command);
}
}
================================================
FILE: src/BusinessLayer/ILogger.cs
================================================
namespace BusinessLayer
{
public interface ILogger
{
void Log(string message);
}
public static class LoggerExtensions
{
public static void LogInformation(this ILogger logger, string messageTemplate, params object[] parameters)
{
// TODO: Use real structured logging here.
logger.Log(messageTemplate);
}
}
}
================================================
FILE: src/BusinessLayer/IQueryHandler.cs
================================================
namespace Contract
{
public interface IQueryHandler where TQuery : IQuery
{
TResult Handle(TQuery query);
}
}
================================================
FILE: src/BusinessLayer/IValidator.cs
================================================
namespace BusinessLayer
{
public interface IValidator
{
/// Validates the given instance.
/// The instance to validate.
/// Thrown when the instance is a null reference.
/// Thrown when the instance is invalid.
void ValidateObject(object instance);
}
}
================================================
FILE: src/BusinessLayer/Properties/AssemblyInfo.cs
================================================
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("BusinessLayer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("BusinessLayer")]
[assembly: AssemblyCopyright("Copyright © 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("06b95749-3b64-47c3-b720-edb06233ed33")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
================================================
FILE: src/BusinessLayer/QueryHandlers/GetOrderByIdQueryHandler.cs
================================================
namespace BusinessLayer.QueryHandlers
{
using System;
using System.Collections.Generic;
using Contract;
using Contract.DTOs;
using Contract.Queries.Orders;
public class GetOrderByIdQueryHandler : IQueryHandler
{
public OrderInfo Handle(GetOrderById query)
{
if (query.OrderId == Guid.Empty)
{
throw new KeyNotFoundException();
}
return new OrderInfo
{
Id = query.OrderId,
CreationDate = DateTime.Today,
TotalAmount = 300
};
}
}
}
================================================
FILE: src/BusinessLayer/QueryHandlers/GetUnshippedOrdersQueryHandler.cs
================================================
namespace BusinessLayer.QueryHandlers
{
using System;
using System.Collections.Generic;
using System.Linq;
using Contract;
using Contract.DTOs;
using Contract.Queries;
using Contract.Queries.Orders;
public class GetUnshippedOrdersQueryHandler : IQueryHandler>
{
private readonly ILogger logger;
public GetUnshippedOrdersQueryHandler(ILogger logger)
{
this.logger = logger;
}
public Paged Handle(GetUnshippedOrders query)
{
this.logger.Log(string.Format("{0} {{ Paging = {{ PageIndex = {1}, PageSize = {2} }} }}",
query.GetType().Name, query.Paging?.PageIndex, query.Paging?.PageSize));
return GetAllOrders().Page(query.Paging);
}
private static IEnumerable GetAllOrders()
{
var random = new Random();
return
from number in Enumerable.Range(1, 100000)
select new OrderInfo
{
Id = Guid.NewGuid(),
TotalAmount = random.Next(100, 1000),
CreationDate = DateTime.Today.AddDays(-number)
};
}
}
}
================================================
FILE: src/BusinessLayer/packages.config
================================================
================================================
FILE: src/Client/App.config
================================================
================================================
FILE: src/Client/Bootstrapper.cs
================================================
namespace Client
{
using Client.Code;
using Client.Controllers;
using Client.CrossCuttingConcerns;
using Contract;
using SimpleInjector;
public static class Bootstrapper
{
private static Container container;
public static void Bootstrap()
{
container = new Container();
container.RegisterInstance(new DynamicQueryProcessor(container));
container.Register(typeof(ICommandHandler<>), typeof(WcfServiceCommandHandlerProxy<>));
container.Register(typeof(IQueryHandler<,>), typeof(WcfServiceQueryHandlerProxy<,>));
container.RegisterDecorator(typeof(ICommandHandler<>),
typeof(FromWcfFaultTranslatorCommandHandlerDecorator<>));
container.Register();
container.Register();
container.Verify();
}
public static TService GetInstance() where TService : class
{
return container.GetInstance();
}
}
}
================================================
FILE: src/Client/Client.csproj
================================================
Debug
x86
8.0.30703
2.0
{9562D251-49BE-4650-93BD-FA6D66DBA61C}
Exe
Properties
Client
Client
v4.8
512
SAK
SAK
SAK
SAK
x86
true
full
false
bin\Debug\
DEBUG;TRACE
prompt
4
true
false
x86
pdbonly
true
bin\Release\
TRACE
prompt
4
true
false
..\packages\Microsoft.Bcl.AsyncInterfaces.1.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll
..\packages\SimpleInjector.5.3.2\lib\net461\SimpleInjector.dll
..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll
..\packages\System.Threading.Tasks.Extensions.4.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll
{DDD88351-9A73-4212-85DC-F769B37D5057}
Contract
================================================
FILE: src/Client/Code/CommandServiceClient.cs
================================================
namespace Client.Code
{
using System.Diagnostics;
using System.ServiceModel;
using Client.Wcf;
// This service reference is hand-coded. This allows us to use our custom KnownCommandTypesAttribute,
// which allows providing WCF with known types at runtime. This prevents us to have to update the client
// reference each time a new command is added to the system.
[KnownCommandTypes]
[ServiceContract(
Namespace = "http://www.solid.net/commandservice/v1.0",
ConfigurationName = "CommandServices.CommandService")]
public interface CommandService
{
[OperationContract(
Action = "http://www.solid.net/commandservice/v1.0/CommandService/Execute",
ReplyAction = "http://www.solid.net/commandservice/v1.0/CommandService/ExecuteResponse")]
object Execute(object command);
}
public class CommandServiceClient : ClientBase, CommandService
{
[DebuggerStepThrough]
public object Execute(object command) => this.Channel.Execute(command);
}
}
================================================
FILE: src/Client/Code/DynamicQueryProcessor.cs
================================================
namespace Client.Code
{
using System.Diagnostics;
using SimpleInjector;
using Contract;
public sealed class DynamicQueryProcessor : IQueryProcessor
{
private readonly Container container;
public DynamicQueryProcessor(Container container)
{
this.container = container;
}
[DebuggerStepThrough]
public TResult Execute(IQuery query)
{
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = this.container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
}
================================================
FILE: src/Client/Code/QueryServiceClient.cs
================================================
namespace Client.Code
{
using System.Diagnostics;
using System.ServiceModel;
using Client.Wcf;
// This service reference is hand-coded. This allows us to use the KnownQueryAndResultTypesAttribute,
// which allows providing WCF with known types at runtime. This prevents us to have to update the client
// reference each time a new command is added to the system.
[KnownQueryAndResultTypes]
[ServiceContract(
Namespace = "http://www.cuttingedge.it/solid/queryservice/v1.0",
ConfigurationName = "QueryServices.QueryService")]
public interface QueryService
{
[OperationContract(
Action = "http://www.cuttingedge.it/solid/queryservice/v1.0/QueryService/Execute",
ReplyAction = "http://www.cuttingedge.it/solid/queryservice/v1.0/QueryService/ExecuteResponse")]
object Execute(object query);
}
public class QueryServiceClient : ClientBase, QueryService
{
[DebuggerStepThrough]
public object Execute(object query) => this.Channel.Execute(query);
}
}
================================================
FILE: src/Client/Code/WcfServiceCommandHandlerProxy.cs
================================================
namespace Client.Code
{
using System;
using System.Diagnostics;
public sealed class WcfServiceCommandHandlerProxy : ICommandHandler
{
[DebuggerStepThrough]
public void Handle(TCommand command)
{
var service = new CommandServiceClient();
try
{
service.Execute(command);
}
finally
{
try
{
((IDisposable)service).Dispose();
}
catch
{
// Against good practice and the Framework Design Guidelines, WCF can throw an
// exception during a call to Dispose, which can result in loss of the original exception.
// See: https://marcgravell.blogspot.com/2008/11/dontdontuse-using.html
// See: https://msdn.microsoft.com/en-us/library/aa355056.aspx
}
}
}
}
}
================================================
FILE: src/Client/Code/WcfServiceQueryHandlerProxy.cs
================================================
namespace Client.Code
{
using System;
using System.Diagnostics;
using Contract;
public sealed class WcfServiceQueryHandlerProxy : IQueryHandler
where TQuery : IQuery
{
[DebuggerStepThrough]
public TResult Handle(TQuery query)
{
var service = new QueryServiceClient();
try
{
return (TResult)service.Execute(query);
}
finally
{
try
{
((IDisposable)service).Dispose();
}
catch
{
// Against good practice and the Framework Design Guidelines, WCF can throw an
// exception during a call to Dispose, which can result in loss of the original exception.
// See: https://marcgravell.blogspot.com/2008/11/dontdontuse-using.html
// See: https://msdn.microsoft.com/en-us/library/aa355056.aspx
}
}
}
}
}
================================================
FILE: src/Client/Controllers/CommandExampleController.cs
================================================
namespace Client.Controllers
{
using System;
using Contract.Commands.Orders;
using Contract.DTOs;
public class CommandExampleController
{
private readonly ICommandHandler createOrderhandler;
private readonly ICommandHandler shipOrderhandler;
public CommandExampleController(
ICommandHandler createOrderhandler,
ICommandHandler shipOrderhandler)
{
this.createOrderhandler = createOrderhandler;
this.shipOrderhandler = shipOrderhandler;
}
public Guid CreateOrder()
{
var createOrderCommand = new CreateOrder
{
NewOrderId = Guid.NewGuid(),
ShippingAddress = new Address
{
Country = "The Netherlands",
City = "Nijmegen",
Street = ".NET Street"
}
};
this.createOrderhandler.Handle(createOrderCommand);
Console.WriteLine("Order with ID {0} has been created.", createOrderCommand.NewOrderId);
return createOrderCommand.NewOrderId;
}
public void ShipOrder(Guid orderId)
{
this.shipOrderhandler.Handle(new ShipOrder { OrderId = orderId });
Console.WriteLine("Order with ID {0} is shipped.", orderId);
}
}
}
================================================
FILE: src/Client/Controllers/QueryExampleController.cs
================================================
namespace Client.Controllers
{
using System;
using System.Linq;
using Contract;
using Contract.DTOs;
using Contract.Queries;
using Contract.Queries.Orders;
public class QueryExampleController
{
private readonly IQueryProcessor queryProcessor;
public QueryExampleController(IQueryProcessor queryProcessor)
{
this.queryProcessor = queryProcessor;
}
public void ShowOrders(int pageIndex, int pageSize)
{
var orders = this.queryProcessor.Execute(new GetUnshippedOrders
{
Paging = new PageInfo { PageIndex = pageIndex, PageSize = pageSize }
});
Console.WriteLine();
Console.WriteLine("Query returned {0} orders: ", orders.Items.Length);
foreach (var order in orders.Items)
{
Console.WriteLine("OrderId: {0}, Amount: {1}, ShipDate: {2:d}",
order.Id, order.TotalAmount, order.CreationDate);
}
Console.WriteLine("Total: " + orders.Items.Sum(order => order.TotalAmount));
}
}
}
================================================
FILE: src/Client/CrossCuttingConcerns/FromWcfFaultTranslatorCommandHandlerDecorator.cs
================================================
namespace Client.CrossCuttingConcerns
{
using System.ComponentModel.DataAnnotations;
using System.ServiceModel;
public class FromWcfFaultTranslatorCommandHandlerDecorator : ICommandHandler
{
private readonly ICommandHandler decoratee;
public FromWcfFaultTranslatorCommandHandlerDecorator(ICommandHandler decoratee)
{
this.decoratee = decoratee;
}
public void Handle(TCommand command)
{
try
{
this.decoratee.Handle(command);
}
catch (FaultException ex) when (ex.Code?.Name == "ValidationError")
{
// The WCF service communicates this specific error back to us in case of a validation
// error. We translate it back to an exception that the client can handle..
throw new ValidationException(ex.Message);
}
}
}
}
================================================
FILE: src/Client/ICommandHandler.cs
================================================
namespace Client
{
public interface ICommandHandler
{
void Handle(TCommand command);
}
}
================================================
FILE: src/Client/IQueryHandler.cs
================================================
namespace Client
{
using Contract;
public interface IQueryHandler where TQuery : IQuery
{
TResult Handle(TQuery query);
}
}
================================================
FILE: src/Client/Program.cs
================================================
namespace Client
{
using System;
using Client.Controllers;
public class Program
{
public static void Main(string[] args)
{
Bootstrapper.Bootstrap();
var orderController = Bootstrapper.GetInstance();
var orderId = orderController.CreateOrder();
orderController.ShipOrder(orderId);
var showUnshippedOrdersController = Bootstrapper.GetInstance();
showUnshippedOrdersController.ShowOrders(pageIndex: 0, pageSize: 10);
Console.ReadLine();
}
}
}
================================================
FILE: src/Client/Properties/AssemblyInfo.cs
================================================
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Client")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Client")]
[assembly: AssemblyCopyright("Copyright © 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3a03603f-acfe-4d73-b924-da926f09a67e")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
================================================
FILE: src/Client/Wcf/KnownCommandTypesAttribute.cs
================================================
namespace Client.Wcf
{
using System;
using System.Collections.Generic;
using System.Linq;
using Contract;
public class KnownCommandTypesAttribute : KnownTypesAttribute
{
public KnownCommandTypesAttribute() : base(new KnownTypesDataContractResolver(CommandTypes))
{
}
private static IEnumerable CommandTypes =>
from type in typeof(Contract.Commands.Orders.CreateOrder).Assembly.GetExportedTypes()
where typeof(ICommand).IsAssignableFrom(type)
where !type.IsAbstract
select type;
}
}
================================================
FILE: src/Client/Wcf/KnownQueryAndResultTypesAttribute.cs
================================================
namespace Client.Wcf
{
using System;
using System.Collections.Generic;
using System.Linq;
using Contract;
public class KnownQueryAndResultTypesAttribute : KnownTypesAttribute
{
public KnownQueryAndResultTypesAttribute()
: base(new KnownTypesDataContractResolver(QueryTypes.Union(ResultTypes)))
{
}
private static IEnumerable ResultTypes => QueryTypes.Select(GetResultType);
private static IEnumerable QueryTypes =>
typeof(Contract.Queries.Orders.GetOrderById).Assembly.GetExportedTypes().Where(IsQueryType);
private static bool IsQueryType(Type type) => GetResultType(type) != null;
private static Type GetResultType(Type queryType) => (
from iface in queryType.GetInterfaces()
where iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IQuery<>)
select iface.GetGenericArguments()[0])
.SingleOrDefault();
}
}
================================================
FILE: src/Client/Wcf/KnownTypesAttribute.cs
================================================
namespace Client.Wcf
{
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)]
public abstract class KnownTypesAttribute : Attribute, IContractBehavior
{
private readonly KnownTypesDataContractResolver resolver;
public KnownTypesAttribute(KnownTypesDataContractResolver resolver)
{
this.resolver = resolver;
}
public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
this.CreateMyDataContractSerializerOperationBehaviors(contractDescription);
}
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint,
DispatchRuntime dispatchRuntime)
{
this.CreateMyDataContractSerializerOperationBehaviors(contractDescription);
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{
}
private void CreateMyDataContractSerializerOperationBehaviors(ContractDescription description)
{
foreach (OperationDescription operationDescription in description.Operations)
{
this.CreateMyDataContractSerializerOperationBehavior(operationDescription);
}
}
private void CreateMyDataContractSerializerOperationBehavior(OperationDescription operationDescription)
{
DataContractSerializerOperationBehavior dataContractSerializerOperationbehavior =
operationDescription.Behaviors.Find();
dataContractSerializerOperationbehavior.DataContractResolver = this.resolver;
}
}
}
================================================
FILE: src/Client/Wcf/KnownTypesDataContractResolver.cs
================================================
namespace Client.Wcf
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Xml;
// source: https://msdn.microsoft.com/en-us/library/dd807519%28v=vs.110%29.aspx
public sealed class KnownTypesDataContractResolver : DataContractResolver
{
private readonly Dictionary knownTypes;
public KnownTypesDataContractResolver(IEnumerable types)
{
this.knownTypes = types.Distinct().ToDictionary(GetName);
}
public override Type ResolveName(
string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
{
try
{
Type type;
return this.knownTypes.TryGetValue(typeName, out type)
? type
: knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Unable to resolve type {typeName}. {ex.InnerException} " +
"If the given type name is postfixed with a weird base64 encoded value, it means that " +
"the type is a generic type. WCF postfixes the name with a hash based on the " +
"namespaces of the generic type arguments. To fix this, mark the class with the " +
"DataContractAttribute to force a specific name. Example:" +
"[DataContract(Name = nameof(YourType) + \"Of{0}\")]. And don't forget to mark the type's " +
"properties with the DataMemberAttribute.", ex);
}
}
[DebuggerStepThrough]
public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver,
out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)
{
if (!knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace))
{
typeName = new XmlDictionaryString(XmlDictionary.Empty, type.Name, 0);
typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, type.Namespace, 0);
}
return true;
}
private static string GetName(Type type) =>
type.IsArray
? "ArrayOf" + GetName(type.GetElementType())
: type.IsGenericType ? GetGenericName(type) : type.Name;
private static string GetGenericName(Type type)
{
Type typeDef = type.GetGenericTypeDefinition();
string name = typeDef.Name.Substring(0, typeDef.Name.IndexOf('`'));
return name + "Of" + string.Join(string.Empty, type.GetGenericArguments().Select(GetName));
}
}
}
================================================
FILE: src/Client/packages.config
================================================
================================================
FILE: src/Contract/Commands/Orders/CreateOrder.cs
================================================
namespace Contract.Commands.Orders
{
using System;
using System.ComponentModel.DataAnnotations;
using Contract.DTOs;
using Contract.Validators;
/// Creates a new order.
public class CreateOrder : ICommand
{
/// The order id of the new order.
[NonEmptyGuid]
public Guid NewOrderId { get; set; }
/// The order's shipping address.
[Required, ValidateObject]
public Address ShippingAddress { get; set; }
}
}
================================================
FILE: src/Contract/Commands/Orders/ShipOrder.cs
================================================
namespace Contract.Commands.Orders
{
using System;
using Contract.Validators;
/// Commands an order to be shipped.
public class ShipOrder : ICommand
{
/// The id of the order.
[NonEmptyGuid]
public Guid OrderId { get; set; }
}
}
================================================
FILE: src/Contract/Contract.csproj
================================================
Debug
AnyCPU
8.0.30703
2.0
{DDD88351-9A73-4212-85DC-F769B37D5057}
Library
Properties
Contract
Contract
v4.8
512
SAK
SAK
SAK
SAK
true
full
false
bin\Debug\
DEBUG;TRACE
prompt
4
false
false
bin\Debug\Contract.XML
pdbonly
true
bin\Release\
TRACE
prompt
4
false
false
bin\Release\Contract.XML
================================================
FILE: src/Contract/DTOs/Address.cs
================================================
namespace Contract.DTOs
{
using System.ComponentModel.DataAnnotations;
public class Address
{
/// The country.
[Required(AllowEmptyStrings = false)]
[StringLength(100)]
public string Country { get; set; }
/// The city.
[Required(AllowEmptyStrings = false)]
[StringLength(100)]
public string City { get; set; }
/// The street name including number.
[Required(AllowEmptyStrings = false)]
[StringLength(100)]
public string Street { get; set; }
}
}
================================================
FILE: src/Contract/DTOs/OrderInfo.cs
================================================
namespace Contract.DTOs
{
using System;
public class OrderInfo
{
public Guid Id { get; set; }
public DateTime CreationDate { get; set; }
public decimal TotalAmount { get; set; }
}
}
================================================
FILE: src/Contract/ICommand.cs
================================================
namespace Contract
{
public interface ICommand { }
}
================================================
FILE: src/Contract/IQuery.cs
================================================
namespace Contract
{
/// Defines a query message.
///
public interface IQuery
{
}
}
================================================
FILE: src/Contract/IQueryProcessor.cs
================================================
namespace Contract
{
public interface IQueryProcessor
{
TResult Execute(IQuery query);
}
}
================================================
FILE: src/Contract/Properties/AssemblyInfo.cs
================================================
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Contract")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Contract")]
[assembly: AssemblyCopyright("Copyright © 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("1281e947-813d-46c8-8b1f-59eba87bb298")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
================================================
FILE: src/Contract/Queries/Orders/GetOrderById.cs
================================================
namespace Contract.Queries.Orders
{
using System;
using Contract.DTOs;
using Validators;
/// Gets order information of a single order by its id.
public class GetOrderById : IQuery
{
/// The id of the order to get.
[NonEmptyGuid]
public Guid OrderId { get; set; }
}
}
================================================
FILE: src/Contract/Queries/Orders/GetUnshippedOrders.cs
================================================
namespace Contract.Queries.Orders
{
using Contract.DTOs;
///
/// Gets a paged list of all unshipped orders for the current logged in user.
///
public class GetUnshippedOrders : IQuery>
{
/// The paging information.
public PageInfo Paging { get; set; }
}
}
================================================
FILE: src/Contract/Queries/PageInfo.cs
================================================
namespace Contract.Queries
{
/// Object containing information about paging.
public class PageInfo
{
/// Returns a PageInfo that represents the request for a single page.
public static PageInfo SinglePage() => new PageInfo { PageIndex = 0, PageSize = -1 };
/// The 0-based page index.
public int PageIndex { get; set; }
/// The number of items in a page.
public int PageSize { get; set; } = 20;
/// Gets the value indicating whether the page info represents the request for a single page.
public bool IsSinglePage() => this.PageIndex == 0 && this.PageSize == -1;
}
}
================================================
FILE: src/Contract/Queries/Paged.cs
================================================
namespace Contract.Queries
{
using System.Runtime.Serialization;
// Applying the DataContract attribute to generic types prevents WCF from postfixing the closed-generic
// type name with a seemingly random hexadecimal code.
/// Contains a set of items for the requested page.
/// The item type.
[DataContract(Name = nameof(Paged) + "Of{0}")]
public class Paged
{
/// Information about the requested page.
[DataMember] public PageInfo Paging { get; set; }
/// The list of items for the given page.
[DataMember] public T[] Items { get; set; }
}
}
================================================
FILE: src/Contract/Validators/CompositeValidationResult.cs
================================================
namespace Contract.Validators
{
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
///
public class CompositeValidationResult : ValidationResult, IEnumerable
{
public CompositeValidationResult(string errorMessage, IEnumerable results)
: base(errorMessage)
{
this.Results = results;
}
public IEnumerable Results { get; }
public IEnumerator GetEnumerator() => this.Results.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}
================================================
FILE: src/Contract/Validators/NonEmptyGuidAttribute.cs
================================================
namespace Contract.Validators
{
using System;
using System.ComponentModel.DataAnnotations;
///
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class NonEmptyGuidAttribute : RequiredAttribute
{
///
public override bool IsValid(object value)
{
if (value == null)
{
return false;
}
if (!(value is Guid))
{
return false;
}
return ((Guid)value) != Guid.Empty;
}
}
}
================================================
FILE: src/Contract/Validators/ValidateObjectAttribute.cs
================================================
namespace Contract.Validators
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
///
public class ValidateObjectAttribute : ValidationAttribute
{
///
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var context = new ValidationContext(value, null, null);
var results = new List();
Validator.TryValidateObject(value, context, results, validateAllProperties: true);
if (results.Count == 0)
{
return ValidationResult.Success;
}
return new CompositeValidationResult(
string.Format("Validation for {0} failed!", validationContext.DisplayName),
results);
}
}
}
================================================
FILE: src/Settings.StyleCop
================================================
NoMerge
is
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
False
True
True
False
False
False
False
False
================================================
FILE: src/SolidServices.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31919.166
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{9F659AB9-6E1D-47EE-8DF8-EC7C593129D7}"
ProjectSection(SolutionItems) = preProject
Settings.StyleCop = Settings.StyleCop
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{9562D251-49BE-4650-93BD-FA6D66DBA61C}"
ProjectSection(ProjectDependencies) = postProject
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1} = {CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WcfService", "WcfService\WcfService.csproj", "{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contract", "Contract\Contract.csproj", "{DDD88351-9A73-4212-85DC-F769B37D5057}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BusinessLayer", "BusinessLayer\BusinessLayer.csproj", "{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiService", "WebApiService\WebApiService.csproj", "{B71A8419-1827-48A4-913E-467B52A7C2F1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebCore3Service", "WebCore3Service\WebCore3Service.csproj", "{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebCore6Service", "WebCore6Service\WebCore6Service.csproj", "{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|Mixed Platforms = Release|Mixed Platforms
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Any CPU.ActiveCfg = Debug|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|Mixed Platforms.Build.0 = Debug|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|x86.ActiveCfg = Debug|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Debug|x86.Build.0 = Debug|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Any CPU.ActiveCfg = Release|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Mixed Platforms.ActiveCfg = Release|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|Mixed Platforms.Build.0 = Release|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|x86.ActiveCfg = Release|x86
{9562D251-49BE-4650-93BD-FA6D66DBA61C}.Release|x86.Build.0 = Release|x86
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Debug|x86.ActiveCfg = Debug|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Any CPU.Build.0 = Release|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{CCFA6865-6B10-4D7C-B6B8-8C37BBFE7DA1}.Release|x86.ActiveCfg = Release|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Debug|x86.ActiveCfg = Debug|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Any CPU.Build.0 = Release|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{DDD88351-9A73-4212-85DC-F769B37D5057}.Release|x86.ActiveCfg = Release|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Debug|x86.ActiveCfg = Debug|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Any CPU.Build.0 = Release|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5E9B468D-FD1B-4AE5-8A10-C9B09CE2690C}.Release|x86.ActiveCfg = Release|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Debug|x86.ActiveCfg = Debug|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Any CPU.Build.0 = Release|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{B71A8419-1827-48A4-913E-467B52A7C2F1}.Release|x86.ActiveCfg = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|x86.ActiveCfg = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Debug|x86.Build.0 = Debug|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Any CPU.Build.0 = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|x86.ActiveCfg = Release|Any CPU
{BB7A06CC-A96E-4B96-B8B6-2E96F3F20783}.Release|x86.Build.0 = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|x86.ActiveCfg = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Debug|x86.Build.0 = Debug|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Any CPU.Build.0 = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|x86.ActiveCfg = Release|Any CPU
{1941CDB2-31FD-4DEC-B0A6-D3FA78431CC9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {57B78032-8930-4257-B608-F0610294E5DE}
EndGlobalSection
EndGlobal
================================================
FILE: src/WcfService/Bootstrapper.cs
================================================
namespace WcfService
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Threading;
using BusinessLayer;
using SimpleInjector;
using WcfService.Code;
using WcfService.CrossCuttingConcerns;
public static class Bootstrapper
{
private static Container container;
public static object GetCommandHandler(Type commandType) =>
container.GetInstance(typeof(ICommandHandler<>).MakeGenericType(commandType));
public static object GetQueryHandler(Type queryType) =>
container.GetInstance(BusinessLayerBootstrapper.CreateQueryHandlerType(queryType));
public static IEnumerable GetCommandTypes() => BusinessLayerBootstrapper.GetCommandTypes();
public static IEnumerable GetQueryAndResultTypes()
{
var queryTypes = BusinessLayerBootstrapper.GetQueryTypes().Select(q => q.QueryType);
var resultTypes = BusinessLayerBootstrapper.GetQueryTypes().Select(q => q.ResultType).Distinct();
return queryTypes.Concat(resultTypes);
}
public static void Bootstrap()
{
container = new Container();
BusinessLayerBootstrapper.Bootstrap(container);
container.RegisterDecorator(typeof(ICommandHandler<>),
typeof(ToWcfFaultTranslatorCommandHandlerDecorator<>));
container.RegisterWcfServices(Assembly.GetExecutingAssembly());
RegisterWcfSpecificDependencies();
container.Verify();
}
public static void Log(Exception ex)
{
Debug.WriteLine(ex.ToString());
}
private static void RegisterWcfSpecificDependencies()
{
container.RegisterInstance(new DebugLogger());
container.Register(() => Thread.CurrentPrincipal);
}
}
}
================================================
FILE: src/WcfService/Code/DebugLogger.cs
================================================
namespace WcfService.Code
{
using System.Diagnostics;
using BusinessLayer;
public sealed class DebugLogger : ILogger
{
public void Log(string message)
{
Debug.WriteLine(message);
}
}
}
================================================
FILE: src/WcfService/Code/WcfExceptionTranslator.cs
================================================
namespace WcfService.Code
{
using System;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel;
public static class WcfExceptionTranslator
{
public static FaultException CreateFaultExceptionOrNull(Exception exception)
{
if (exception is ValidationException)
{
return new FaultException(
new ValidationError { ErrorMessage = exception.Message }, exception.Message);
}
#if DEBUG
return new FaultException(exception.ToString());
#else
return null;
#endif
}
}
}
================================================
FILE: src/WcfService/CommandService.svc
================================================
<%@ ServiceHost
Language="C#"
Debug="true"
Service="WcfService.CommandService"
CodeBehind="CommandService.svc.cs"
%>
================================================
FILE: src/WcfService/CommandService.svc.cs
================================================
namespace WcfService
{
using System;
using System.Collections.Generic;
using System.Reflection;
using System.ServiceModel;
using Code;
[ServiceContract(Namespace = "http://www.solid.net/commandservice/v1.0")]
[ServiceKnownType(nameof(GetKnownTypes))]
public class CommandService
{
public static IEnumerable GetKnownTypes(ICustomAttributeProvider provider) =>
Bootstrapper.GetCommandTypes();
[OperationContract]
[FaultContract(typeof(ValidationError))]
public void Execute(dynamic command)
{
try
{
dynamic commandHandler = Bootstrapper.GetCommandHandler(command.GetType());
commandHandler.Handle(command);
}
catch (Exception ex)
{
Bootstrapper.Log(ex);
var faultException = WcfExceptionTranslator.CreateFaultExceptionOrNull(ex);
if (faultException != null)
{
throw faultException;
}
throw;
}
}
}
}
================================================
FILE: src/WcfService/CrossCuttingConcerns/ToWcfFaultTranslatorCommandHandlerDecorator.cs
================================================
namespace WcfService.CrossCuttingConcerns
{
using System.ComponentModel.DataAnnotations;
using System.ServiceModel;
using BusinessLayer;
using Contract;
public class ToWcfFaultTranslatorCommandHandlerDecorator : ICommandHandler
where TCommand : ICommand
{
private readonly ICommandHandler decoratee;
public ToWcfFaultTranslatorCommandHandlerDecorator(ICommandHandler decoratee)
{
this.decoratee = decoratee;
}
public void Handle(TCommand command)
{
try
{
this.decoratee.Handle(command);
}
catch (ValidationException ex)
{
// This ensures that validation errors are communicated to the client,
// while other exceptions are filtered by WCF (if configured correctly).
throw new FaultException(ex.Message, new FaultCode("ValidationError"));
}
}
}
}
================================================
FILE: src/WcfService/Global.asax
================================================
<%@ Application Codebehind="Global.asax.cs" Inherits="WcfService.Global" Language="C#" %>
================================================
FILE: src/WcfService/Global.asax.cs
================================================
namespace WcfService
{
using System;
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
Bootstrapper.Bootstrap();
}
}
}
================================================
FILE: src/WcfService/NonDotNetQueryService.cs
================================================
================================================
FILE: src/WcfService/NonDotNetQueryService.svc
================================================
<%@ ServiceHost Language="C#" Debug="true" Service="WcfService.NonDotNetQueryService" CodeBehind="NonDotNetQueryService.cs" %>
================================================
FILE: src/WcfService/NonDotNetQueryService.tt
================================================
<#
/*
Generates a service class for queries to be consumed by non-.NET clients.
*/
#>
<#@ template language="C#" debug="true" hostspecific="true" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ assembly name="VSLangProj" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ output extension=".cs" #>
<#
// To debug, uncomment the next two lines !!
// System.Diagnostics.Debugger.Launch();
// System.Diagnostics.Debugger.Break();
Initialize(this);
var queryTypes = GetAllQueryTypes(ContractProject).ToArray();
var referencedNamespaces = GetAllReferencedNamespaces(queryTypes);
#>
//
#pragma warning disable 1591
namespace WcfService
{
using System.ServiceModel;
using Contract;
<# foreach (var referencedNamespace in referencedNamespaces) { #>
using <#=referencedNamespace #>;
<#} #>
[ServiceContract(Namespace = "http://www.cuttingedge.it/solid/queryservice/v1.0")]
public class NonDotNetQueryService
{
<#
foreach (var queryType in queryTypes)
{
var @interface = GetQueryInterfaceForCodeClass(queryType);
var resultType = GetQueryResultType(@interface);
#> [OperationContract]
[FaultContract(typeof(ValidationError))]
public <#= resultType #> <#= queryType.Name.Replace("Query", "") #>(<#= queryType.Name #> query) => Execute(query);
<# } #>
private static TResult Execute(IQuery query) => (TResult)QueryService.ExecuteQuery(query);
}
}
<#+
const string QueryInterface = "IQuery";
static DTE Dte;
static Project CurrentProject;
static Project ContractProject;
static TextTransformation TT;
static string T4FileName;
static string T4Folder;
// static string GeneratedCode = @"GeneratedCode(""SolidServices"", ""1.0"")";
static Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
void Initialize(TextTransformation tt) {
TT = tt;
T4FileName = Path.GetFileName(Host.TemplateFile);
T4Folder = Path.GetDirectoryName(Host.TemplateFile);
// Get the DTE service from the host
var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null) {
Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
}
// Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe
if (Dte == null) {
throw new Exception("This template can only be executed through the Visual Studio host");
}
CurrentProject = GetProjectContainingT4File(Dte);
ContractProject = GetContractProject(Dte);
}
Project GetProjectContainingT4File(DTE dte) {
// Find the .tt file's ProjectItem
ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
// If the .tt file is not opened, open it
if (projectItem.Document == null)
projectItem.Open(Constants.vsViewKindCode);
// Mark the .tt file as unsaved. This way it will be saved and update itself next time the
// project is built. Basically, it keeps marking itself as unsaved to make the next build work.
// Note: this is certainly hacky, but is the best I could come up with so far.
projectItem.Document.Saved = false;
return projectItem.ContainingProject;
}
Project GetContractProject(DTE dte)
{
string queryCsFile = QueryInterface + ".cs";
ProjectItem projectItem = dte.Solution.FindProjectItem(queryCsFile);
if (projectItem == null) {
Error("Could not find the VS Project containing the " + queryCsFile + " file.");
return null;
}
return projectItem.ContainingProject;
}
IEnumerable GetAllQueryTypes(params Project[] projects)
{
return
from Project project in projects
from ProjectItem projectItem in project.ProjectItems
from type in GetAllQueryTypesRecursive(projectItem)
select type;
}
IEnumerable GetAllQueryTypesRecursive(ProjectItem projectItem)
{
var queryTypes = GetAllQueryTypes(projectItem);
var recursiveQueryTypes =
from ProjectItem subItem in projectItem.ProjectItems
from type in GetAllQueryTypesRecursive(subItem)
select type;
return queryTypes.Union(recursiveQueryTypes);
}
IEnumerable GetAllQueryTypes(ProjectItem projectItem)
{
if (projectItem.FileCodeModel == null)
{
return Enumerable.Empty();
}
var elements = projectItem.FileCodeModel.CodeElements.OfType