4

I have a C# ASP.NET project with 15 WCF/ASMX Service References. Each service is deployed to three different servers; test, staging and live.

Often we need to change the URLs of these service references to different servers, to be able to debug using the correct service with the correct implementation and data.

When managing these Service References I'm having a hard time keeping the URL in sync. When creating a Service Reference FooService it stores the URL in three seperate files:

  • FooService.disco
  • configuration.svcinfo
  • Reference.svcmap

Along with creating an endpoint node with the URL in the Web.config.

If I change the endpoint URL in the Web.config and rebuild the project, it doesn't update the URL in the other files, so it's out of sync. So when I right-click the FooService and click Update Service Reference it doesn't use the URL stored in the Web.config, it uses the URL in the other files.

So the only way is to right-click the FooService and click on Configure Service Reference and enter the new URL. But that doesn't always work, because sometimes it creates a new node in Web.config named FooService1, so I get an error when running my application saying that there are two instances of the same endpoints.

So often I need to skim through the Web.config and delete duplicates of endpoints, which is very frustrating.

What is the best way to manage multiple WCF service references when changing the URL to the service often?

Gaui
  • 8,723
  • 16
  • 64
  • 91
  • 6
    Don't use WCF, it's bad practice. – Phill Apr 28 '14 at 02:59
  • @Phill surely you mean don't use ASMX?!? Everything I have read is to use WCF over ASMX. A quick summary here: http://stackoverflow.com/questions/2448472/what-is-the-difference-between-wcf-and-asmx-web-services. Please substantiate your claim! – Jon P Apr 28 '14 at 04:15
  • 6
    @JonP - Sure if you want to follow the MS Enterprise way of creating a mess, use WCF. But its a pile of shit. We have Web API, Nancy, ServiceStack. WCF needs to die, ASAP. Its a configuration nightmare. The only benefit is TCP bindings. Absolutely everything else is absolute rubbish. – Phill Apr 28 '14 at 06:33
  • 1
    @JonP - your answer proves my point. Configuration nightmare. – Phill Apr 28 '14 at 06:36
  • 1
    I second what @Phill says - avoid avoid avoid. Use Nancy IMO. Please. Avoid WCF. – Pure.Krome Apr 28 '14 at 06:44
  • 1
    @Phill your argument would probably hold more water if it was presented as a reasoned argument and less of a rant. Further more the practicalities are that many of us have WCF services in place and replacing them is prohibitive. If it was a greenfields project fine, but the OP has 15 WCF/ASMX services in-place, ID doubt that refactoring those to an alternate framework is usefull to the OP at this point in time. – Jon P Apr 28 '14 at 07:08
  • 2
    @JonP - Its a configuration nightmare. Debugging is painful. Error handling is painful. Dependency injection is painful. Intercepting is painful. It's bloated. It's slow. It can cause IIS to crash under load. Its fiddly. Time consuming. Data Contracts are painful. Attributes... attributes everywhere... Hard to use JSON. Doesn't support conneg. Painful to implement any form of authentication. – Phill Apr 28 '14 at 07:18

3 Answers3

8

First an overview

The way I handle it is to not add the service reference via Visual Studio. What I have is each service has it's own Proxy project in the solution, to proxy is created using a simple one line batch file. Each of these is then included in the website(s) as a "vanilla" reference. The end point is configured manually in the web config. I have three deployment options: Debug (local), Staging and Release. At the web.config level the different addresses are handled by web.config transformations . The proxy project files are configured so that the correct end point address is used depending on the solution configuration. However it is important to note that web.config transformations only apply on publishing the solution.

In pre-VS2010 I had 3 variations of the web.config files which would overwrite the active web.config; however overwriting the web.config in this way always felt "risky" to me. In this case I think that using transforms for publish will still work, but in the actual web.config file have block of connections that you can comment in and out on those occasions when you want to debug the staging or development servers.

I only have 2 services, so my set up was pretty easy, with 15 there will be a fair bit of work involved to set up, but it will save you head aches in the long run.

Implementation

Back Up Everything First!!

Also keep handy a copy of the existing web.config to help with configuring the end points later on.

Also note that the batch files won't work if your solution is in a path with a space in its name, e.g. the default location VS puts its' projects. All my projects are in the following structure C:\Source\vs2008\, C:\Source\vs2010\ etc. I'll see if I can find a better solution for this.

0 Run VisualStudio as an administrator

Right click Visual Studio from the start menu and select "Run as Administrator". We need to do this as VS will be writing files.

1 Delete Existing service references

You shouldn't need any help on this one.

2 Add Proxy Projects

Doing this now means you only need configure the solution for test|staging|live once.

From the File menu select "Add" then "New Project" and from unser the "Visual C#" tmplates select "Class Library" and name it something sensible. I'll be using FooService.proxy for this example. Rename the class file, I'll be using FooService_proxy.cs in this example.

Add the following references to the project:

  • System.Runtime.Serialization
  • System.ServiceModel

Add one project for each service. We will comeback and update these projects later.

3 Configure The Solution to handle test|staging|live

I'm assuming you use test when developing the asp.net website on your local machine.

Open the "Configuration Manager" by selecting it in the Build Configuration dropdown menu.

Under the "Active Solution Configuration" dropdown select "New"

For the Name I'd suggest "Staging" and check the "Create new project configurations" check box.

In the Solution Explorer, right click on Web.Config and select "Add config transforms". This will add a new file for the staging web.config transformation. Click the expander arrow and you will see three sub files: Web.Debug.Config, Web.Release.Config, Web.Staging Config.

4 Set Up Proxies

Add a batch file to each proxy project by Clicking on the project in the solution explorer and selecting "Add > New Item". Use a text file and name it "CreateProxy.bat.

Insert the following into the new file and save:

:: ============================================================================================
:: Create the proxy file from the service wsdl
:: Input parameters
:: SDK Path The location of svcutil.exe
:: WSDL File Arg1 (%1)
:: Output Proxy .CS file Arg2 (%2)
::
:: Called by the build process of the BeforeBuild target to re-gen the proxy code.
:: Make sure to change FooService.proxy
:: ============================================================================================

svcutil %1 /ct:System.Collections.Generic.List`1 /serializer:DataContractSerializer  /tcv:Version35  /n:*,FooService.Proxy /out:%2

Now right click on the proxy project and click "Unload Project", saving if prompted. This will enable us to get in and modify the project file. Right Click the now greyed out proxy project name and select "Edit".

Add the following just befor the closing </project> tag. Note you may need to change the path SDKPath depending on your location of svcutil. Also make sure to name FooService_proxy.cs whatever you named the proxy file.

<PropertyGroup>
  <!-- These properties are used by the svcutil batch file task in the BeforeBuild Target to regen the proxy code -->

  <SDKPath Condition="'$(SDKPath)'==''">C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin</SDKPath>
  <WSDLPath Condition="'$(WSDLPath)'=='' and '$(Configuration)' == 'Debug'">http://[Path to TEST Server Service]</WSDLPath>
  <WSDLPath Condition="'$(WSDLPath)'=='' and '$(Configuration)' == 'Staging'">http://[Path to STAGING server Service]</WSDLPath>
  <WSDLPath Condition="'$(WSDLPath)'=='' and '$(Configuration)' == 'Release'">http://[Path to LIVE Server Service]</WSDLPath>
  <SkipProxyRegen Condition="'$(SkipProxyRegen)'==''">false</SkipProxyRegen>
</PropertyGroup>
<Target Name="BeforeBuild">
  <Message Importance="normal" Text="Rebuilding the proxy code from batch file '$(ProjectDir)CreateProxy.bat'" Condition="'$(SkipProxyRegen)'=='false'" />
  <Exec Command="$(ProjectDir)CreateProxy.bat $(WSDLPath) $(ProjectDir)FooService_proxy.cs" WorkingDirectory="$(SDKPath)" Condition="'$(SkipProxyRegen)'=='false'" />
</Target>

Save the changes and then right click on the greyed out project name and select "Reload Project".

Right click on the project and select build, make sure that your proxy file is now populated.

Set each proxy project so that it always build into only its' \bin directory, e.g. not bin\debug etc. Right click on the proxy project and select "Properties". Select the "Build" tab and change the "Configuration" drop down to "All Configurations". Set the "Output path" to bin\

5 Add Proxy References and End Points To Your Website

Add a reference to each proxy project to your website by right clicking "References > Add reference" and then going to the "Projects" Tab.

Now open up your web.config file and add the bindings, use your previous web.config as a guide.

<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="fooServiceEndpoint" closeTimeout="00:01:00" openTimeout="00:01:00"
                receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false"
                bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                maxBufferSize="2147483647" maxBufferPoolSize="524288" maxReceivedMessageSize="2147483647"
                messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
                useDefaultWebProxy="true">
                <readerQuotas maxDepth="32" maxStringContentLength="2147483647" maxArrayLength="2147483647"
                    maxBytesPerRead="8192" maxNameTableCharCount="2147483647" />
                <security mode="None">
                    <transport clientCredentialType="None" proxyCredentialType="None"
                        realm="" />
                    <message clientCredentialType="UserName" algorithmSuite="Default" />
                </security>
            </binding>
            <!-- Insert other binding as required -->
        </basicHttpBinding>
    </bindings>
    <client>
        <!-- Test Server Endpoints - Used for day-to-day development -->
        <endpoint address="http:[Path to TEST Server service]"
        binding="basicHttpBinding" bindingConfiguration="fooServiceEndpoint"
        contract="FooService.Proxy.IFooService" name="fooServiceEndpoint" />
        <!-- Add Other endpoints as required -->

        <!-- Staging Server End Points - Used Occasionaly
        <endpoint address="http:[Path to STAGING Server service]"
        binding="basicHttpBinding" bindingConfiguration="fooServiceEndpoint"
        contract="FooService.Proxy.IFooService" name="fooServiceEndpoint" />
        <other end points here too />
        -->

        <!-- LIVEServer End Points - Used Rarely and with CAUTION
        <endpoint address="http:[Path to LIVE Server service]"
        binding="basicHttpBinding" bindingConfiguration="fooServiceEndpoint"
        contract="FooService.Proxy.IFooService" name="fooServiceEndpoint" />
        <other end points here too />
        -->
    </client>
</system.serviceModel>

Now you can simply modify the web config with comments depending on which server you want to be debugging on.

6 Set up web.config transformation for deployment

Expand the web.config node in the solution explorer.

Open the web.staging.config file and add the following:

<system.serviceModel>
    <client>
        <endpoint address="http:[Path to STAGING server Service]"
        binding="basicHttpBinding" bindingConfiguration="fooServiceEndpoint"
        contract="FooService.Proxy.IFooService" name="fooServiceEndpoint" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
    </client>
    <!-- Repeat for additional end points -->
</system.serviceModel>

Now add the same to Web.Release.Config changing the paths to tho the LIVE server paths. This will now use the appropriate endpoints when published using the VisualStudio publish command.

E.g. If deploying a STAGING version of the web site, select "Staging" from the Build Configuration drop down. Right click on the WebSite project in the solution explorer and select "Publish". Choose how you wish to publish and click the "Publish" button. The whole solution will then rebuild, proxies will be generated form the Staging server and the web.config file will be published with the Staging setting.

That's it, you're done

You now have proxies that will generate based on your build configuration, one location to change paths via commenting for debugging purposes, and automatic web.config updating on publishing.

Update

Gaui, the OP, has created a small exe that simplifies this somewhat. It is available on GitHub.

BSMP
  • 4,596
  • 8
  • 33
  • 44
Jon P
  • 19,442
  • 8
  • 49
  • 72
  • I would like to keep the URL only in Web.config. Is that at all possible? – Gaui Jun 03 '14 at 21:44
  • 2
    @Gaui, I'm not sure. Are you hesitant to use config transformations? That is the best way to handle deployments. If you're concenerend about having them in the project file you may be able to create a more in depth BeforeBuild action that would read the appropriate config transform file and extract the URL from there. It's not someting I have tired, and for me the existing solution works pretty well. – Jon P Jun 03 '14 at 23:16
  • 1
    I'm using config transforms so that might work! I can call some script with the $(Configuration) and it will read Web.*.config and call svcutil with the appropriate URL. Thank you for this enlightenment. – Gaui Jun 04 '14 at 10:02
  • 1
    Hello again Jon P, here's my solution: https://github.com/gaui/WCF-Proxy-Generator ... Yet again, thank you! – Gaui Jun 10 '14 at 21:49
4

I've had a lot of problems with this, too. But I ended up in finding a nice and easy way. My example with a fictious service address:

  • Create a command line EXE
  • Add service reference with address https://service.somePortal.com/FooConnector.svc and namespace "MyServiceReference"
  • Let's say the service is offering an Interface "IFooConnector". Choose OK to generate (a lot of) code to consume the service.

After that, in your App.config file you will see a new serviceModel section:

<system.serviceModel>
    <bindings>
        <customBinding>
            <binding name="NetHttpsBinding_IFooConnector">
                <binaryMessageEncoding />
                <httpsTransport />
            </binding>
        </customBinding>
    </bindings>
    <client>
        <endpoint address="https://service.somePortal.com/FooConnector.svc"
            binding="customBinding" bindingConfiguration="NetHttpsBinding_IFooConnector"
            contract="MyServiceReference.IFooConnector" name="NetHttpsBinding_IFooConnector" />
    </client>
</system.serviceModel>

You can now use a service method like this:

using TestClient.MyServiceReference;

namespace TestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var client = new FooConnector())
            {
                client.DoSomething();
            }
        }
    }
}

Now the important part:

To use three inkarnations of the same service, like DEV (Development), TEST (Testing) and PROD (Production) at different addresses, but having the same interface, you only have to manually edit your App.config and use a different constructor to instantiate the client!


Here's the new App.config with the changed serviceModel section:

<system.serviceModel>
    <bindings>
        <customBinding>
            <binding name="NetHttpsBinding_IFooConnector">
                <binaryMessageEncoding />
                <httpsTransport />
            </binding>
        </customBinding>
    </bindings>
    <client>
        <endpoint address="https://dev-service.somePortal.com/FooConnector.svc"
            binding="customBinding" bindingConfiguration="NetHttpsBinding_IFooConnector"
            contract="MyServiceReference.IFooConnector" name="DEV" />

        <endpoint address="https://test-service.somePortal.com/FooConnector.svc"
            binding="customBinding" bindingConfiguration="NetHttpsBinding_IFooConnector"
            contract="MyServiceReference.IFooConnector" name="TEST" />

        <endpoint address="https://service.somePortal.com/FooConnector.svc"
            binding="customBinding" bindingConfiguration="NetHttpsBinding_IFooConnector"
            contract="MyServiceReference.IFooConnector" name="PROD" />
    </client>
</system.serviceModel>

As you see, we're now having three endpoint sections with different service addresses. I've also changed the endpoints name properties to match my desired DEV, TEST and PROD naming.

To call the required service you can now use a different constructor of the client, having one parameter: string endpointConfigurationName. So you can now use the same service method in its three inkarnations like this:

using TestClient.MyServiceReference;

namespace TestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var client = new FooConnector("DEV"))
            {
                //Call method in DEV
                client.DoSomething();
            }

            using (var client = new FooConnector("TEST"))
            {
                //Call method in TEST
                client.DoSomething();
            }

            using (var client = new FooConnector("PROD"))
            {
                //Call method in PROD
                client.DoSomething();
            }
        }
    }
}

That's it! :-)

P.S.: In my real project, I have an enum user setting to choose DEV, TEST, PROD to change the used service by configuration.

jreichert
  • 1,466
  • 17
  • 31
  • in your real project, how many places in your code to you have to use a switch on the appSetting value of DEV|TEST|PROD ? – qxotk Nov 01 '22 at 14:13
  • 1
    @qxotk I don't really understand the question. In my real project there is ONE helper function to get the right FooConnector based on the environment setting DEV, TEST, PROD. – jreichert Nov 07 '22 at 07:25
  • You answered my question: 1 place in the source, you check the appSetting value. Then, anywhere you need a FooConnector, you have to say whether you want the DEV|TEST|PROD FooConnector - or your default constructor can check the appSetting value and call the correct specialized constructor, is that right? – qxotk Nov 17 '22 at 17:55
  • 1
    @qxotk Correct. – jreichert Nov 21 '22 at 07:12
2

One technique is to get the endpoint and replace the URL in code (e.g., you can do it from a database), with something like this:

endpoint.Endpoint.Address = new System.ServiceModel.EndpointAddress(remoteUrl);
Andy
  • 414
  • 4
  • 8
  • Would this be a simplification of the approach posted by @jreichert ? So the endpoint is set at run time vs. build time - and you would keep all 3 DEV|TEST|PROD endpoints being built into the final product in PROD (as well as DEV and TEST)? – qxotk Nov 01 '22 at 14:16