Motivation
For the desktop world, wcf remains the most common way to organize client-server interaction in .net for both local and global networks. It is flexible to configure, easy to use and transparent.
At least it should be. In practice, adding a new service is a routine. You need to remember to register the configuration on the server, do the same on the client, you need to write or generate a proxy class. Maintain configs uncomfortable. If the service has changed, then you need to make changes to the proxy class. And do not forget about registration in the IoC container. And adding new hosts for new services. And I want a simple asynchrony. Separately, everything is simple, but even for the article I added this list three times already, and I am not sure that I did not miss something.
Time to automate. The simplest scenario from creating a solution to calling a wcf service looks like this:
- Install-Package Rikrop.Core.Wcf.Unity
- We write ServiceContract and their implementation
- On the server and the client, we add one registration line to the IoC (no need to edit the configs)
- We lift hosts from two lines
var assembly = Assembly.GetExecutingAssembly(); _serviceHostManager.StartServices(assembly);
- On the client, resolve the IServiceExecutor <TService>. This wrapper serves to call the service methods and hides the work with the channel.
- Can use
var articles = await _myServiceExecutor.Execute(service => service.GetArticles());
Quick start
Create a client-server application. Clients send the server the index of the number from the Fibonacci sequence, the server returns the number from the sequence with the specified index.
Logging and error handling is removed from the code in the article, but in the code on github I give a more complete example to illustrate the holistic approach.Project structure close to reality:
')

Server.Contracts contains the interfaces of wcf-services, Server - their implementation, as well as the implementation of the hoster - a class that will raise the wcf-services. BL - server logic. ConsoleServiceHost hosts services in the domain of the console application. Client.Presentaion contains the corresponding client layer. In our example, there is only a command to call the service and process the result. Client is a console application that uses the previous build to handle user input.
Actually, nuget packages need to be installed as follows:
- Rikrop.Core.Wcf.Unity contains helpers for registering the infrastructure necessary for wcf operation in the IoC container. This is a set of ready-made solutions and extensions for quick setup of all aspects of interaction. The package should be added to projects where there will be server and client registrations in the IoC container. We have this RikropWcfExample.Server and RikropWcfExample.Client.
- Rikrop.Core.Wcf contains the main classes for working with wcf, channel management, sessions, authorization, hosting wcf-services. We’ll add it to RikropWcfExample.Server, there will be a hoster, and RikropWcfExample.Client.Presentation *, where the wcf-service call will come from.
In RikropWcfExample.Server.Contracts add the description of the
wcf-service :
using System.ServiceModel; using System.Threading.Tasks; namespace RikropWcfExample.Server.Contracts { [ServiceContract] public interface ICalculatorService { [OperationContract] Task<ulong> GetFibonacciNumber(int n); } }
The implementation in
CalculatorService.cs will transmit the request and return the result from the business logic layer:
using RikropWcfExample.Server.BL; using RikropWcfExample.Server.Contracts; using System.Threading.Tasks; namespace RikropWcfExample.Server { public class CalculatorService : ICalculatorService { private readonly FibonacciCalculator _fibonacciCalculator; public CalculatorService(FibonacciCalculator.ICtor fibonacciCalculatorCtor) { _fibonacciCalculator = fibonacciCalculatorCtor.Create(); } public async Task<ulong> GetFibonacciNumber(int n) { return await _fibonacciCalculator.Calculate(n); } } }
While you can see one feature - wcf-service uses async / await to describe asynchrony. For the rest, there are no specific constructions.
We now turn to registration. The simplest syntax for the server indicates the type of binding (NetTcp) a list of behaviors that should be added to the services:
private static IUnityContainer RegisterWcfHosting(this IUnityContainer container, string serviceIp, int servicePort) { container .RegisterServerWcf( o => o.RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort)) .RegisterServiceHostFactory(reg => reg.WithBehaviors().AddDependencyInjectionBehavior()) ); return container; }
The client specifies the type of service wrapper for services (ServiceExecutor), the type of wrapper over the binding (Standart assumes NetTcp) and, in fact, the server address:
private static IUnityContainer RegisterWcf(this IUnityContainer container, string serviceIp, int servicePort) { container .RegisterClientWcf(o => o.RegisterServiceExecutor(reg => reg.Standard() .WithExceptionConverters() .AddFaultToBusinessConverter()) .RegisterChannelWrapperFactory(reg => reg.Standard()) .RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort))); return container; }
Everything. You do not need to register each service by interface, you do not need to create a proxy, you do not need to register wcf in the configuration - these registrations will allow you to immediately start working with services as if they are local calls.
But first you need to kill them on the server. The
Rikrop.Core.Wcf library already includes the
ServiceHostManager class, which does all the work on its own. Register each service is not necessary:
using Rikrop.Core.Wcf; using System.Reflection; namespace RikropWcfExample.Server { public class WcfHoster { private readonly ServiceHostManager _serviceHostManager; public WcfHoster(ServiceHostManager serviceHostManager) { _serviceHostManager = serviceHostManager; } public void Start() { var assembly = Assembly.GetExecutingAssembly(); _serviceHostManager.StartServices(assembly); } public void Stop() { _serviceHostManager.StopServices(); } } }
Start the
server :
public static void Main() { using (var serverContainer = new UnityContainer()) { serverContainer.RegisterServerDependencies(); var service = serverContainer.Resolve<WcfHoster>(); service.Start(); Console.WriteLine(" . Enter."); Console.ReadLine(); service.Stop(); } }
Run the
client :
static void Main() { using (var container = new UnityContainer()) { container.RegisterClientDependencies(); var calculateFibonacciCommandCtor = container.Resolve<CalculateFibonacciCommand.ICtor>(); int number; while (int.TryParse(GetUserInput(), out number)) { var command = calculateFibonacciCommandCtor.Create(); var result = command.Execute(number); Console.WriteLine("Fibonacci[{0}] = {1}", number, result); } } }
Works:

Comparison with the classic approach and extensibility
It may seem that the proposed solution requires quite a lot of infrastructure code and does not have any advantages over the usual use of wcf. The easiest way to show the difference is the example of typical situations that arise when working on projects.
Adding a new method to an existing wcf-service or changing the signature of an existing method
Rikrop.Core.Wcf (.Unity) | Without using libraries |
---|
- In ServiceContract add method definition.
- In the class of the wcf-service add implementation.
Now you can call a new method on the client. | - In ServiceContract add method definition.
- In the class of the wcf-service add implementation.
- Generate a proxy class on the client or add a manual implementation of calling a new service on the client (sometimes both options if you do not want to directly use the proxy class.
Now you can call a new service on the client. |
Adding a new wcf service to an existing host
Rikrop.Core.Wcf (.Unity) | Without using libraries |
---|
- Create a ServiceContract new service.
- Implement a service contract.
Now you can call a new service on the client. | - Create a ServiceContract new service.
- Implement a service contract.
- Generate a proxy class on the client or add a manual implementation of calling a new service on the client (sometimes both options if you do not want to directly use the proxy class.
- Add wcf code to the host that initializes the ServiceHost for the new service.
- Register in the client IoC container the proxy class of the new service.
- Add service configuration on server and non-client.
Now you can call a new service on the client. |
Change settings of all wcf-services (for example, the type of binding)
Rikrop.Core.Wcf (.Unity) | Without using libraries |
---|
- In server registration, change the line with the binding type *.
- In client registration, change the string with the type of binding type *.
* see public Result Custom <TServiceConnection> (LifetimeManager lifetimeManager = null, params InjectionMember [] injectionMembers) where TServiceConnection: IServiceConnection | - In the server's app.config, change all entries in the <bindings> * block.
- In the client’s app.config, change all entries in the <bindings> block *.
* The amount of work is proportional to the number of wcf-services. If there are 100, then we can only hope that the quick replacement of the file will work. |
Change settings of several wcf-services (for example, the type of binding)
Rikrop.Core.Wcf (.Unity) | Without using libraries |
---|
- On the server, add registration for the new address and type of binding.
- In the client registration add a new registration for a different address and type of binding.
| - In the server's app.config, change the entries in the <bindings> block for the desired wcf-services.
- In the client’s app.config, change the entries in the <bindings> block for the desired wcf services.
|
It is worth saying a few words about the last two points. With the proper organization of app.config, making changes to it is pretty easy. This can be done without rebuilding the application. In a real development, the structured wcf configuration comes across quite rarely, which is the reason for the iterative development. A non-programmer also has to change the configuration infrequently if the initial settings meet the requirements. At the same time, it is easy to make a typo that the compiler will not find.
Extensibility Behavior for authorization and work with sessions
Expansion of functionality and behavior change occurs by adding Behavior during registration. The most frequent use is the behavior responsible for transmitting session information in the wcf message header.
To demonstrate the functionality, a separate
branch was created with the expanded code of the previous example. In the standard behavior setting, the developer is asked to select the authorization method - this is the OperationContract, which will be available to users without a session in the message header. Calling the remaining methods will be possible only with a filled header.
Registration on the server will look like this:
container .RegisterType<ISessionResolver<Session>, SessionResolver<Session>>() .RegisterServerWcf( o => o.RegisterServiceConnection(reg => reg.NetTcp(serviceIp, servicePort)) .RegisterServiceHostFactory(reg => reg.WithBehaviors() .AddErrorHandlersBehavior(eReg => eReg.AddBusinessErrorHandler().AddLoggingErrorHandler(NLogger.CreateEventLogTarget())) .AddDependencyInjectionBehavior() .AddServiceAuthorizationBehavior(sReg => sReg.WithStandardAuthorizationManager() .WithStandardSessionHeaderInfo("ExampleNamespace", "SessionId") .WithOperationContextSessionIdInitializer() .WithSessionAuthStrategy<Session>() .WithLoginMethod<ILoginService>(s => s.Login()) .WithOperationContextSessionIdResolver() .WithInMemorySessionRepository() .WithStandardSessionCopier()) ) );
You can change the authorization method by adding your
System.ServiceModel.ServiceAuthorizationManager implementation, change the session identifier initialization method, authorization verification method, session retrieval method from the request execution context, and how the sessions are stored and copied on the server. In a generic case, the registration of AuthorizationBehavior might look like this:
.AddServiceAuthorizationBehavior(sReg => sReg.WithCustomAuthorizationManager<ServiceAuthorizationManagerImpl>() .WithCustomSessionHeaderInfo<ISessionHeaderInfoImpl>() .WithCustomSessionIdInitializer<ISessionIdInitializerImpl>() .WithCustomAuthStrategy<IAuthStrategyImpl>() .WithLoginMethod<ILoginService>(s => s.Login()) .WithCustomSessionIdResolver<ISessionIdResolverImpl>() .WithCustomSessionRepository<ISessionRepositoryImpl<MySessionImpl>>() .WithCustomSessionCopier<ISessionCopierImpl<MySessionImpl>>())
Client registration also changes:
private static IUnityContainer RegisterWcf(this IUnityContainer container, string serviceIp, int servicePort) { container .RegisterType<ClientSession>(new ContainerControlledLifetimeManager()) .RegisterClientWcf(o => o .RegisterServiceExecutor(reg => reg.Standard() .WithExceptionConverters() .AddFaultToBusinessConverter()) .RegisterChannelWrapperFactory(reg => reg.Standard()) .RegisterServiceConnection(reg => reg .NetTcp(serviceIp, servicePort) .WithBehaviors() .AddSessionBehavior(sReg => sReg .WithStandardSessionHeaderInfo("ExampleNamespace", "SessionId") .WithCustomSessionIdResolver<ClientSession>(new ContainerControlledLifetimeManager()) .WithStandardMessageInspectorFactory<ILoginService>(service => service.Login())))); return container; }
Result:

Work algorithm
- The client is authorized through the selected method in the wcf-contract . If authentication is successful, the server creates a session, saves it in the repository and gives data about it to the client:
var newSession = Session.Create(userId); _sessionRepository.Add(newSession); return new SessionDto { SessionId = newSession.SessionId, Username = "ExampleUserName" };
- The client receives the session data and saves it:
var clientSession = container.Resolve<ClientSession>(); var sessionDto = Task.Run(async () => await loginServiceExecutor.Execute(s => s.Login())).Result; clientSession.Session = sessionDto;
- The server has the ability to obtain information about the calling client:
public async Task<ulong> GetFibonacciNumber(int n) { var session = _sessionResolver.GetSession(); _logger.LogInfo( string.Format("User with SessionId={0} and UserId={1} called CalculatorService.GetFibonacciNumber", session.SessionId, session.UserId)); return await _fibonacciCalculator.Calculate(n); }
- The client has the opportunity to receive data received from the server during authorization:
_logger.LogInfo(string.Format("SessionId {0} with name {1} begin calculate Fibomacci", _clientSession.SessionId, _clientSession.Session.Username));
What's inside
Most of the infrastructure provides the library System.ServiceModel.dll. However, there are several solutions that need to be considered in more detail.
The basis of the interaction between the client and the server is the implementation of the
IServiceExecutor interface, located in the Rikrop.Core.Wcf library.
public interface IServiceExecutor<out TService> { Task Execute(Func<TService, Task> action); Task<TResult> Execute<TResult>(Func<TService, Task<TResult>> func); }
In the
simplest case , the channel is opened and the method is called in the context of this channel:
public async Task<TResult> Execute<TResult>(Func<TService, Task<TResult>> func) { using (var wrapper = _channelWrapperFactory.CreateWrapper()) { return await func(wrapper.Channel); } }
More complex implementations can
convert errors or additionally notify about the end of processing by changing the property. These ideas are most widely used in WPF-implementations of the IServiceExecutor, where using ServiceExecutorFactory, you can create wrappers over a wcf-service, allowing you to use DataBinding to notify the UI of a lengthy operation, or display a popup with arbitrary information while waiting for a response from the server.
For an easy implementation, the main role is played by the Fluent interface during registration and the standard library infrastructure implementations, which makes it even easy to figure out even the most complex structures from the first time using the studio jump:

The article also indirectly mentions other libraries:
- Implementation of auto plants :
private static IUnityContainer RegisterFactories(this IUnityContainer container) { new[] { Assembly.GetExecutingAssembly(), typeof (FibonacciCalculator).Assembly } .SelectMany(assembly => assembly.DefinedTypes.Where(type => type.Name == "ICtor")) .Where(type => !container.IsRegistered(type)) .ForEach(container.RegisterFactory); return container; }
- Wrappers over loggers :
private static IUnityContainer RegisterLogger(this IUnityContainer container) { container.RegisterType<ILogger>(new ContainerControlledLifetimeManager(), new InjectionFactory(f => NLogger.CreateConsoleTarget())); return container; }
Results
Once you have set up the infrastructure on a project, you can forget about the network nature of the interaction through the IServiceExexutor for a long time. It is best to use a systematic approach and use the same framework for building desktop applications using the mvvm pattern,
interacting with the database , logging and other typical tasks. But even with the reluctance to use an unfamiliar and not always familiar framework, you can find an application to the ideas underlying it. Component extensibility, strict typing during configuration, interaction transparency on all layers, minimization of the infrastructure code and time spent on maintaining the infrastructure are important things to keep in mind when writing a calculator and a multi-user Enterprise system. You can download the library code and connect it to the solution by the project instead of using the library. This will allow you to study the work under the debugger and make changes if necessary.
Bonus
There is nothing better than practice. I learned that we had the experience of translating a fairly large project (~ 300.000 lines of code) somewhere between development and support for using Rikrop.Core.Wcf. This is a rather interesting experience of tormenting with async / await in .net 4.0, customizing work with sessions, extracting settings from a config and translating them into a c # form. If anyone is interested, you can describe a specific example of switching to this library without dragging out the entire framework.
There is also a solution for wpf with informing the user via ui blocking or pop-up windows implemented via ServiceExecutorFactory. This is a private example and it applies much more to wpf than wcf. But it can give more information about the benefits of the library and the motivation to use.