earthcoreThis is a short step-by-step guide on creating a tridion core web service, which is basically a service that communicates with the Content Manager server. Please note that you need to have a basic knowledge of ASP.NET WebAPI (or other web-application technology), as well as good understanding of the tridion core framework. The source code for this demo is the web service for our BDelete GUI extension , which is used to delete an unpublished component and all its children. More info about GUI extensions can be found here.

Source code on github

1. Set-up the project

So lets start by creating a new WebAPIproject. If you are going to use it mainly for the GUI extensions, you can name it something like “TridionGUIExtensionsServices”. After the project is created, there a few very important set-up tasks that need to be performed.

  • Add reference to the core-service client dll, located in your Tridion install folder (e.g. Program Files x86\Tridion)
  • Enable cross-origin request sharing (CORS) – this is needed if you want to test the web-service from a Tridion extension. If you don’t plan to use the service from an outside source you can skip directly to step 2. Otherwise open your packet manager console use the command:  Install-Package Microsoft.AspNet.WebApi.Cors
  • In order to enable CORS, go to your project/App_Start/WebApiConfig and add this line: config.EnableCors(). 
  • Now that this is done, you can use attributes on your controller (or on individual actions) to allow CORS.
[EnableCors(origins: "*", headers: "*", methods: "*")]
    public class ValuesController : ApiController
    {

2. Configure the application

First you need to define the application end-points for connecting with Tridion.  The following code should be added to the configuration section in Web.config.

<system.serviceModel>
    <!-- Default/example WCF settings for Core Service. These settings should be copied into the host application's configuration file. -->
    <bindings>
      <!-- Default Core Service binding settings are provided here. These can be used as a starting point for further customizations. -->
      <basicHttpBinding>
        <binding name="basicHttp" maxReceivedMessageSize="10485760">
          <readerQuotas maxStringContentLength="10485760" maxArrayLength="10485760"/>
          <security mode="TransportCredentialOnly">
            <!-- For LDAP or SSO authentication of transport credentials, use clientCredentialType="Basic" -->
            <transport clientCredentialType="Windows"/>
          </security>
        </binding>
        <binding name="streamDownload_basicHttp" maxReceivedMessageSize="209715200" transferMode="StreamedResponse" messageEncoding="Mtom" sendTimeout="00:10:00">
          <security mode="TransportCredentialOnly">
            <!-- For LDAP or SSO authentication of transport credentials, use clientCredentialType="Basic" -->
            <transport clientCredentialType="Windows"/>
          </security>
        </binding>
        <binding name="streamUpload_basicHttp" maxReceivedMessageSize="209715200" transferMode="StreamedRequest" messageEncoding="Mtom" receiveTimeout="00:10:00">
          <security mode="None"/>
        </binding>
      </basicHttpBinding>
      <wsHttpBinding>
        <binding name="wsHttp" transactionFlow="true" maxReceivedMessageSize="10485760">
          <readerQuotas maxStringContentLength="10485760" maxArrayLength="10485760"/>
          <security mode="Message">
            <message clientCredentialType="Windows"/>
          </security>
        </binding>
      </wsHttpBinding>
      <netTcpBinding>
        <binding name="netTcp" transactionFlow="true" transactionProtocol="OleTransactions" maxReceivedMessageSize="10485760">
          <readerQuotas maxStringContentLength="10485760" maxArrayLength="10485760"/>
        </binding>
        <binding name="streamDownload_netTcp" maxReceivedMessageSize="2147483647" transferMode="StreamedResponse" sendTimeout="00:10:00"/>
        <binding name="streamUpload_netTcp" maxReceivedMessageSize="2147483647" transferMode="StreamedRequest" receiveTimeout="00:10:00"/>
      </netTcpBinding>
      <!-- Default binding settings for SAML compliant federated authentication -->
      <ws2007FederationHttpBinding>
        <binding name="wsFederationHttp" transactionFlow="true" maxReceivedMessageSize="10485760">
          <readerQuotas maxStringContentLength="10485760" maxArrayLength="10485760"/>
          <security mode="Message">
            <!-- For asymmetric key configuration, use issuedKeyType="AsymmetricKey" -->
            <!-- For SAML 1.1 compliance, use issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" -->
            <message negotiateServiceCredential="false" issuedKeyType="SymmetricKey" issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0"/>
          </security>
        </binding>
      </ws2007FederationHttpBinding>
      <customBinding>
        <binding name="netFederationTcp" receiveTimeout="00:10:00">
          <transactionFlow transactionProtocol="OleTransactions"/>
          <security authenticationMode="SecureConversation" requireSecurityContextCancellation="true" requireSignatureConfirmation="false">
            <secureConversationBootstrap authenticationMode="IssuedTokenForCertificate" requireSignatureConfirmation="false">
              <!-- For asymmetric key configuration, use keyType="AsymmetricKey" and remove the keySize attribute -->
              <!-- For SAML 1.1 compliance, use tokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" -->
              <issuedTokenParameters tokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" keySize="256" keyType="SymmetricKey"/>
            </secureConversationBootstrap>
          </security>
          <binaryMessageEncoding>
            <readerQuotas maxStringContentLength="10485760" maxArrayLength="10485760"/>
          </binaryMessageEncoding>
          <tcpTransport/>
        </binding>
      </customBinding>
    </bindings>
    <client>
      <!--
      Default Core Service endpoint settings are provided here. The endpoint name should be specified when constructing a proxy service instance.
      The mapping between proxy service types and applicable endpoint names is as follows (see also the contracts specified on each endpoint):
      CoreServiceClient: basicHttp
      SessionAwareCoreServiceClient: wsHttp, netTcp
      StreamDownloadClient: streamDownload_basicHttp, streamDownload_netTcp
      StreamUploadClient: streamUpload_basicHttp, streamUpload_netTcp
      -->
      <endpoint name="basicHttp_2013" address="http://localhost/webservices/CoreService2013.svc/basicHttp" binding="basicHttpBinding" bindingConfiguration="basicHttp" contract="Tridion.ContentManager.CoreService.Client.ICoreService"/>
      <endpoint name="streamDownload_basicHttp_2013" address="http://localhost/webservices/CoreService2013.svc/streamDownload_basicHttp" binding="basicHttpBinding" bindingConfiguration="streamDownload_basicHttp" contract="Tridion.ContentManager.CoreService.Client.IStreamDownload"/>
      <endpoint name="streamUpload_basicHttp_2013" address="http://localhost/webservices/CoreService2013.svc/streamUpload_basicHttp" binding="basicHttpBinding" bindingConfiguration="streamUpload_basicHttp" contract="Tridion.ContentManager.CoreService.Client.IStreamUpload"/>
      <!-- endpoint name="batch_basicHttp_2013" address="http://localhost/webservices/CoreService2013.svc/batch_basicHttp" binding="basicHttpBinding" bindingConfiguration="basicHttp" contract="Tridion.ContentManager.CoreService.Client.ICoreServiceBatch" /-->
      <endpoint name="wsHttp_2013" address="http://localhost/webservices/CoreService2013.svc/wsHttp" binding="wsHttpBinding" bindingConfiguration="wsHttp" contract="Tridion.ContentManager.CoreService.Client.ISessionAwareCoreService">
        <identity>
          <dns value="localhost"/>
        </identity>
      </endpoint>
      <endpoint name="netTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/netTcp" binding="netTcpBinding" bindingConfiguration="netTcp" contract="Tridion.ContentManager.CoreService.Client.ISessionAwareCoreService"/>
      <endpoint name="streamDownload_netTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/streamDownload_netTcp" binding="netTcpBinding" bindingConfiguration="streamDownload_netTcp" contract="Tridion.ContentManager.CoreService.Client.IStreamDownload"/>
      <endpoint name="streamUpload_netTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/streamUpload_netTcp" binding="netTcpBinding" bindingConfiguration="streamUpload_netTcp" contract="Tridion.ContentManager.CoreService.Client.IStreamUpload"/>
      <endpoint name="batch_netTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/batch_netTcp" binding="netTcpBinding" bindingConfiguration="netTcp" contract="Tridion.ContentManager.CoreService.Client.ICoreServiceBatch"/>
      <!-- Default endpoint settings for SAML compliant federated authentication -->
      <endpoint name="wsSamlHttp_2013" address="http://localhost/webservices/CoreService2013.svc/wsFederationHttp" binding="ws2007FederationHttpBinding" bindingConfiguration="wsFederationHttp" contract="Tridion.ContentManager.CoreService.Client.ISessionAwareCoreService">
        <identity>
          <dns value="localhost"/>
        </identity>
      </endpoint>
      <endpoint name="netSamlTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/netFederationTcp" binding="customBinding" bindingConfiguration="netFederationTcp" contract="Tridion.ContentManager.CoreService.Client.ISessionAwareCoreService"/>
      <endpoint name="batch_netSamlTcp_2013" address="net.tcp://localhost:2660/CoreService/2013/batch_netFederationTcp" binding="customBinding" bindingConfiguration="netFederationTcp" contract="Tridion.ContentManager.CoreService.Client.ICoreServiceBatch"/>
    </client>
    <!--
    Use these behavior settings for SAML compliant federated authentication.
    Configure the clientCertificate and serviceCertificate - adjust the store locations, store names and subject names for your certificates.
    Client certificate is used to issue SAML tokens, and service certificate represents the target CoreService host.

    <behaviors>
      <endpointBehaviors>
        <behavior>
          <clientCredentials type="Tridion.ContentManager.CoreService.Client.Security.ClaimsClientCredentials, Tridion.ContentManager.CoreService.Client" supportInteractive="false">
            <clientCertificate storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName" findValue="SamlTokenIssuer" />
            <serviceCertificate>
              <defaultCertificate storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName" findValue="localhost" />
            </serviceCertificate>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    -->
  </system.serviceModel>

3. Create the TridionRepository class, which is the layer that communicates with the Core Service.  I will not go into details, but the main property of this class is the SessionAwareCoreServiceClientIt requires a valid endpoint parameter (see the endpoint names that we added to the web.config), and you can see in the source code that I used the value “netTcp_2013”, as this is the fastest most reliable endpoint (however, if port 2660 is blocked, we can use the http binding). There are also 3 important methods for the purpose of the demo: GetLocalizedItems, IsPublished, UnlocalizeItem, DeleteItem. The names are self-explanatory.

4. Create the “Delete” Action in the Controller:

 public HttpResponseMessage Delete(string id)
        {
            XElement localItem = repository.GetLocalizedItems("tcm:" + id);
            if (localItem == null)
            {
                return new HttpResponseMessage(HttpStatusCode.NotFound);
            }
            if (repository.IsPublished("tcm:" + id))
            {
                return this.Request.CreateResponse(HttpStatusCode.Forbidden, "The component is published");
            }

            var childNodes = localItem.Elements();
            if (childNodes.Count() != 0)
            {
                bool hasPublishedChildren = false;
                foreach (var node in childNodes)
                {
                    if (repository.IsPublished(node.Attribute("ID").Value))
                    {
                        hasPublishedChildren = true;
                        break;
                    }
                }

                if (hasPublishedChildren)
                {
                    return this.Request.CreateResponse(HttpStatusCode.Forbidden, "The component has published children");
                }

                foreach (var node in childNodes)
                {
                    repository.UnlocalizeItem(node.Attribute("ID").Value);
                    //repository.DeleteItem(node.Attribute("ID").Value);
                }
            }

            repository.UnlocalizeItem("tcm:" + id);
            //repository.DeleteItem("tcm:" + id);

            return new HttpResponseMessage(HttpStatusCode.OK);
        }

Basically, when you make a DELETE request to the server, with the component id as a parameter (“tcm:” is ommited from the request because it causes errors), the following happens:

  • The core service looks for the id of the component and if nothing is found the server returns a NotFound response.
  • If the component is published, the server returns a Forbidden reponse. I am not 100% that this is the most correct response type, but it should give you a clue why the request was not accepted.
  • If the component exists and is not published, the server checks if it has any children that are published. If so, you get the same response as above
  • If all the checks pass, then the application loops through all the components’ children and calls Unlocalize and Delete commands on them. Finally, the same commands are called for the original item and the server returns a OK response.
  • Please note that if the component is included in any page, the Delete will fail (unlocalizing will still work though)!

That is all. Now you can use Fiddler to test the service. If your routes are set the same as mine (look in App_Start\WebApiConfig) your request will be something like: api\values\delete\4-71

Author: Stanislav Stoychev