Dot-Net

在 ASP.NET Core 站點中使用 Reporting Services (SSRS) 作為參考

  • November 21, 2018

幾天來我一直在努力尋找解決方案,所以我想分享我的情況。我正在將現有的 ASP.NET MVC 應用程序轉換為 ASP.NET Core MVC。然而,使用 ASP.NET Core 的最大變化是System.Web命名空間是不行的。然而,通常情況下,SQL Server Reporting Services (SSRS) 通常作為 WebReference 添加到項目中,該項目基於 - 你猜對了,System.Web.

所以我需要找到一種替代方法來訪問 SSRS 端點來執行報告。對於我的場景,我主要想要 PDF(儘管呼叫 SSRS 的Render方法允許您選擇導出格式)。

這個問題的解決方案提出了它自己的問題,最明顯的是一個錯誤:

Microsoft.ReportingServices.Diagnostics.Utilities.MissingSessionIdException:缺少會話標識符。此操作需要會話標識符。

所以我最終回答的兩個可能對其他人有價值的問題是,如何在沒有 System.Web 的情況下使用 SSRS,以及如何解決有關*“缺少會話標識符”的錯誤*

我用來解決這個問題的第一步是 Visual Studio 的Connected Services和 WCF。此方法生成一些類似於 WebReferences 但基於System.DataModel而不是System.Web. SvcUtil.exe我應該注意,如果出於某種原因您沒有 Visual Studio,您可以使用該工俱生成這些相同的類。

使用 VS2017 <15.5 時,需要從Visual Studio Marketplace中獲取用於添加 WCF 服務引用的擴展。對於 VS2017 >= 15.5,它現在內置在. 之後,右鍵點擊Connected Service時,您應該有一個新條目,稱為Add Connected Service…。下一個螢幕上的條目之一應該是Microsoft WCF Web 服務參考提供程序(在撰寫本文時,該擴展處於預覽狀態)。輸入您的服務端點的 URI,對我來說是 的形式http://[SERVERNAME-OR-IP]/ReportServer/ReportExecution2005.asmx?wsdl,並在底部設置您的命名空間。我保留了所有其他預設值,然後點擊完成. 我不記得當我第一次得到這個設置時我在那個領域使用了什麼,但我希望它[MyProjectNamespace].ReportingServices在它完成時使用。

這將為您提供您的課程。

在此處輸入圖像描述

同樣,這也可以使用SvcUtil.exe來完成。

除了我的新參考之外,我使用的程式碼/類如下。我盡力使程式碼盡可能全面。我的實際實現進行了更多重構,但它只是增加了複雜性,而無需掌握這一切是如何工作的。所以我試圖讓這段程式碼盡可能線性。如果我的程式碼有錯誤,請隨時告訴我:-)

public async Task&lt;byte[]&gt; RenderReport(string report, IDictionary&lt;string, object&gt; parameters, string exportFormat = null)
{
   //My binding setup, since ASP.NET Core apps don't use a web.config file
   var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportCredentialOnly);
   binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Ntlm;
   binding.MaxReceivedMessageSize = 10485760; //I wanted a 10MB size limit on response to allow for larger PDFs

   //Create the execution service SOAP Client
   var rsExec = new ReportExecutionServiceSoapClient(binding, new EndpointAddress(reportingServicesUrl));

   //Setup access credentials. I use windows credentials, yours may differ
   var clientCredentials = new NetworkCredential(reportingServicesUserName, reportingServicesPassword, reportingServicesDomain);
   rsExec.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
   rsExec.ClientCredentials.Windows.ClientCredential = clientCredentials;

   //This handles the problem of "Missing session identifier"
   rsExec.Endpoint.Behaviors.Add(new ReportingServicesEndpointBehavior());

   //Load the report
   var taskLoadReport = await rsExec.LoadReportAsync(report, null);

   //Set the parameteres asked for by the report
   var reportParameters = taskLoadReport.Parameters.Where(x =&gt; parameters.ContainsKey(x.Name)).Select(x =&gt; new ParameterValue() { Name = x.Name, Value = parameters[x.Name].ToString() }).ToArray();
   await rsExec.SetExecutionParametersAsync(reportParameters, "en-us");

   //run the report
   const string deviceInfo = @"&lt;DeviceInfo&gt;&lt;Toolbar&gt;False&lt;/Toolbar&gt;&lt;/DeviceInfo&gt;";
   var response = await rsExec.RenderAsync(new RenderRequest(exportFormat ?? "PDF", deviceInfo));

   //spit out the result
   return response.Result;
}

其中大部分是不言自明的,但我想指出我正在添加的端點行為。請參閱,當載入報告詳細資訊並隨後使用該資訊使用我在參數中的值設置報告的參數時…以及參數,然後呈現報告,您需要設置會話標識符以連接呼叫成為同一會話上下文的所有部分。它要查找的會話標識符是一個名為 ExecutionHeader 的 SOAP 標頭值,其值稱為“ExecutionID”。這是在對我的電話的回應中提供的LoadReportAsync,但不會自動轉移到以後對 API 的所有呼叫中。我已經嘗試了多種方法來做到這一點,但由於固有的類試圖將 XML 命名空間設置為我想要的以外的東西而遇到了問題。最終,EndpointBehavior 是侵入性最小的解決方案(也是我唯一使用的解決方案)。支持這個的類看起來像這樣。

using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

internal class ReportingServicesEndpointBehavior : IEndpointBehavior
{
   public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

   public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
   {
       clientRuntime.ClientMessageInspectors.Add(new ReportingServicesExecutionInspector());
   }

   public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }

   public void Validate(ServiceEndpoint endpoint) { }
}

internal class ReportingServicesExecutionInspector : IClientMessageInspector
{
   private MessageHeaders headers;

   public void AfterReceiveReply(ref Message reply, object correlationState)
   {
       var index = reply.Headers.FindHeader("ExecutionHeader", "http://schemas.microsoft.com/sqlserver/2005/06/30/reporting/reportingservices");
       if (index &gt;= 0 && headers == null)
       {
           headers = new MessageHeaders(MessageVersion.Soap11);
           headers.CopyHeaderFrom(reply, reply.Headers.FindHeader("ExecutionHeader", "http://schemas.microsoft.com/sqlserver/2005/06/30/reporting/reportingservices"));
       }
   }

   public object BeforeSendRequest(ref Message request, IClientChannel channel)
   {
       if(headers != null)
           request.Headers.CopyHeadersFrom(headers);

       return Guid.NewGuid(); //https://msdn.microsoft.com/en-us/library/system.servicemodel.dispatcher.iclientmessageinspector.beforesendrequest(v=vs.110).aspx#Anchor_0
   }
}

這裡有兩個班;一個是 EndpointBehavior,另一個是 MessageInspector。EndpointBehavior 的唯一目的是連接 MessageInspector。我找不到繞過那個額外步驟的方法。但是 MessageInspector 所做的是,每次返迴響應時,如果我們還沒有從過去的響應中保存 ExecutionHeader,我們會從該響應中保存一個。隨後,每次我們發送請求時,如果我們從過去的響應中保存了一個 ExecutionHeader,我會將它附加到這個新請求的標頭中。通過這種方式,我確保命名空間和圍繞此會話標識符的所有其他復雜性正是服務提供它們的方式,因此我盡我所能相信它們將是有效的。

希望這可以幫助任何尋找解決方案的人。我在網上看到了很多關於該主題的其他問題,但沒有一個有我需要的答案/解釋。

引用自:https://stackoverflow.com/questions/44036903