Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Null Return, when WCF Client [.Net8.0] communicate to WCF Server [.Net Framework 4.8] #5699

Open
robert-naumann opened this issue Nov 26, 2024 · 3 comments
Labels

Comments

@robert-naumann
Copy link

robert-naumann commented Nov 26, 2024

Describe the bug
When I call an async method on my service it works in most environments. But in one system (productive) I've got always null return instead of the expected data container. It makes no different which method I'll call. All of them return null. No exceptions when calling. Same code on both installations (dev and production).

Additional context

  • Used client lib version:
<PackageReference Include="System.ServiceModel.Duplex" Version="6.0.0" />
<PackageReference Include="System.ServiceModel.NetTcp" Version="8.0.0" />
<PackageReference Include="System.ServiceModel.Federation" Version="8.0.0" />
  • NetTcp Binding (dual) is configured. App.config of server:
<?xml version="1.0"?>
<configuration>
    <system.serviceModel>

        <behaviors>
            <serviceBehaviors>
                <behavior name="MyServiceBehavior">
                    <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8088/App.svc" />
                    <serviceDebug includeExceptionDetailInFaults="true" />
                    <dataContractSerializer maxItemsInObjectGraph="10485760" />
                    <serviceSecurityAudit auditLogLocation="Application" serviceAuthorizationAuditLevel="Failure" messageAuthenticationAuditLevel="Failure" suppressAuditFailure="true" />                    
                  </behavior>          
            </serviceBehaviors>
        </behaviors>

        <bindings>
            <netTcpBinding>
                <binding name="WebApiConfig" closeTimeout="00:10:00"
                    openTimeout="00:10:00" sendTimeout="00:10:00" maxReceivedMessageSize="2147483647">
                    <readerQuotas maxDepth="64" maxStringContentLength="10485760"
                        maxArrayLength="2147483647" maxBytesPerRead="10485760"
                        maxNameTableCharCount="16384" />
                    <security mode="None">
                        <message clientCredentialType="None" algorithmSuite="Default" />
                    </security>
                </binding>
            </netTcpBinding>
        </bindings>

        <services>
            <service behaviorConfiguration="MyServiceBehavior" name="MyApp">
                <!-- Here are some more defined WSDualHttp bindings and 2 more netTcp Bindings (MsgPack and WindowsAuth configured behavior)  -->
                <endpoint address="net.tcp://localhost:8198" binding="netTcpBinding" bindingConfiguration="WebApiConfig" name="WebApiEndpoint" contract="MyApp.IServiceContract">
                </endpoint>
            </service>
        </services>
    
    </system.serviceModel>
</configuration>
  • ServiceClient init:
using System.ServiceModel;
using System.ServiceModel.Channels;

public partial class MyServiceClient : DuplexClientBase<MyApp.IServiceContract>, IServiceContract, IDisposable
{
    public DualServiceClientImpl(string url, string endPointName, CallbackHandler callbackhandler)
#if NETFRAMEWORK
        : base(callbackhandler, endPointName, new EndpointAddress(url))
#else
        : base(new InstanceContext(callbackhandler), CreateBinding(endPointName), new EndpointAddress(url))
#endif
    { }

    private static Binding CreateBinding(string endPointName, SecurityMode securityMode = SecurityMode.None) 
    =>  new NetTcpBinding() { 
        CloseTimeout = TimeSpan.FromMinutes(10),
        OpenTimeout = TimeSpan.FromMinutes(10),
        SendTimeout= TimeSpan.FromMinutes(10),
        MaxReceivedMessageSize = Int32.MaxValue,
        ReaderQuotas = new System.Xml.XmlDictionaryReaderQuotas()
        {
            MaxDepth = 64,
            MaxStringContentLength = MAX_QUOTA_LENGTH,
            MaxArrayLength= MAX_QUOTA_LENGTH,
            MaxBytesPerRead= MAX_QUOTA_LENGTH,
            MaxNameTableCharCount = 16384
        },
        Security = CreateBindingSecurity(securityMode)
    };

    const int MAX_QUOTA_LENGTH = 10485760;

    private static NetTcpSecurity CreateBindingSecurity(SecurityMode securityMode)
    {
        switch (securityMode)
        {
            // Works not in .Net Core
            case SecurityMode.Message:
                return new NetTcpSecurity { 
                    Mode = SecurityMode.Message, 
                    Message = new MessageSecurityOverTcp { ClientCredentialType = MessageCredentialType.Windows } 
                };
            case SecurityMode.None:
                return new NetTcpSecurity { Mode = SecurityMode.None };
            default: throw new NotImplementedException();
        }
    }
}

Please help! I can't see the error.

@HongGit HongGit added the triaged label Dec 3, 2024
@mconnew
Copy link
Member

mconnew commented Dec 3, 2024

What do you mean when you say "I've got always null return instead of the expected data container"? This will require adding some instrumentation and/or attaching a debugger to understand what's going on. Knowing more precisely what you are experiencing will help to narrow down what steps you can take to get more information. Are you experiencing the null on the service side or the client? Are you able to attach a debugger while running your app in the problem environment? If so, are there are exceptions which are thrown and handled so that your code doesn't get exposed to them?

@robert-naumann
Copy link
Author

robert-naumann commented Dec 4, 2024

I'm sorry. I can't debug here. This is customer land 😊. May this code sample explains a littlebit more:

SampleDataContainer.cs:

[DataContract]
public class SampleDataContainer {
    [DataMember]
    public List<string> SomeData { get; set; }
    [DataMember]
    public List<DatabaseModel> SomeMoreData { get; set; }
}

IServiceContract.cs:

[ServiceContract(Namespace = "http://mycompany.intranet.int",
				Name = "myService",
				CallbackContract = typeof(IServiceContractCallback))]
public interface IServiceContract {

        [FaultContract(typeof(BusinessFault))]
        [OperationContract]
        SampleDataContainer FetchSomeData();
        
        // All methods exists twice as sync/async
        [FaultContract(typeof(BusinessFault))]
        [OperationContract]
        Task<SampleDataContainer> FetchSomeData_async();
}

ServerImpl.cs:

public class ServerImpl : IServiceContract {
    public Task<SampleDataContainer> FetchSomeData_async()
    => Task.FromResult(FetchSomeData());

    public SampleDataContainer FetchSomeData() 
    => new SampleDataContainer() {
        SomeData = new List<string> { "some", "sample", "data" },
        SomeMoreData = LoadDataFromDB()
    };
    
    private List<DatabaseModel> LoadDataFromDB() {
        // some deeper code; returns rows from DB
    }
}

ServiceContractCallback.cs:

public interface IServiceContractCallback {
    [OperationContract(IsOneWay = true)]
    void SendMessage(CallbackMessageData messageData);
}

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)] 
public class ServiceContractCallbackClientImpl : IServiceContractCallback {
    // ...
}

MyClientApp.cs:

IServiceContract service = CreateServiceClient();
SampleDataContainer data = await service.FetchSomeData_async();
// This is where problem kicks in. All calls before are fine. data is null. The server does always return something, but the client didn't receive it
if (data == null || data.SomeData == null || data.SomeMoreData == null)
    // BOOM🧨
    throw new NullReferenceException($"data == null: {data == null}; data.SomeData == null: {data?.SomeData == null}; data.SomeMoreData == null: {data?.SomeMoreData == null}");

// The error output is:
// data == null: True; data.SomeData == null: True; data.SomeMoreData == null: True

public IServiceContract CreateServiceClient(string url = "net.tcp://localhost:8089") 
=> MyServiceClient(url, "netTcpBinding", new ServiceContractCallbackClientImpl());

@mconnew
Copy link
Member

mconnew commented Dec 4, 2024

Before we get into your problem, I just wanted to explain to you the whole sync vs async thing in the contract interface as you're doing something a little unusual and you might be able to improve/cleanup your code a little. Unrelated to your problem, but wanted to mention it. Whether the operation is sync or async doesn't change anything sent or received over the wire. This means you can have your server side operation be synchronous while having the client be async, or vice versa. There is a default action and reply action value generated which is based off the method name. If your method is synchronous and called Foo, it will have the same action value generated as an async method call FooAsync. If an operation method returns Task or Task<T>, then it removes any trailing Async from the name before generating the action, which results in the same action name. What this means is on the client side you can declare two methods, Foo and FooAsync, and as long as they have the same list of parameters and return the same type (with the async version having the return value wrapped in a Task), then they will dispatch to the same operation on the service side. So you can offer a Foo and FooAsync method on your client side contract interface so the client can decide whether to call your operation synchronously or asynchronously, and they will dispatch to a single service side method. You can't declare both on the service side contract because WCF wouldn't know whether to dispatch the call to the synchronous implementation or the async implementation as they are identical from a SOAP message standpoint.
On the service side, if your implementation is actually synchronous, just declare the synchronous variant of the implementation and calls get dispatched there regardless of whether the client called your service using the sync or async variant (it's identical on the wire).
So in your sample above, the client could declare SampleDataContainer FetchSomeData() and Task<SampleDataContainer> FetchSomeDataAsync() on the client side interface, and the service would just declare SampleDataContainer FetchSomeData(). At the moment you have two distinct operations, one called FetchSomeData, and one called FetchSomeData_async (because you didn't suffix with the case sensitive name "Async").

Anyway, on to your problem. Having a null value returned means the XML being returned isn't matching what is expected. In the response, there will be a wrapping response XML element, probably called FetchSomeDataResponse which should contain a FetchSomeDataResult xml element which corresponds to the SampleDataContainer type serialized with that name. When you get a null return value, it's usually because the FetchSomeDataResult object is misnamed and not what's expected. The typical situation which leads to this is when you change the service contract in a way which changes the name, and you use a mismatched client and server. So the service writes the SOAP reply message with one name for the XML element, and the client is looking for a different name, which isn't there, so results in a null value.
I'm not sure if that's your case though. It looks like you provided a contract which isn't your actual service contract (presumably to not reveal info about your business) as I doubt you actually have an operation called FetchSomeData. If this is really representative, then it's not a mismatch of versions. The reason is to modify the names of the XML elements in the return message would require changing the name of the method (only other way is to use MessageContractAttribute which your code snippet doesn't do), and changing the name of the method changes the default action name (and you aren't specifying your own action name in the OperationContract attribute which is what would be needed). If you change the action name, you'll get an exception about a contract filter mismatch and the operation won't dispatch. So if the contract really is representative of how your contract looks (with just the names changed to protect the innocent), then a mismatched service and client would break a different way.

Are you able to provide your customer with a custom build of your app which has some extra instrumentation added, and then collect some logs and have them send you the log file? If so, I can show you how to capture the XML SOAP message in a text form and you can then use that any way which makes sense to you (write to file, put into zip file etc).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants