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

Feature | Add SqlDataSourceEnumerator #1430

Merged
merged 36 commits into from Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
dcd629b
Added UDP Broadcast to SSRP
Kaur-Parminder Dec 13, 2021
742107c
Added SqlDataSourceEnumerator Public API and AppContext for Helpers (…
Kaur-Parminder Dec 21, 2021
8374240
Added AppContext change to unit test
Kaur-Parminder Dec 21, 2021
d629a1c
Increased recieve buffer size to match to native recieve buffer size …
Kaur-Parminder Feb 7, 2022
0bc15f8
reset to d629a1c
Kaur-Parminder Feb 7, 2022
68ae3ba
Merge remote-tracking branch 'Upstream/main' into MSDEnumerator
Kaur-Parminder Feb 7, 2022
e2cca89
Added stopwatch to track time elapsed for max timeouts in recieve dat…
Kaur-Parminder Feb 7, 2022
f47181a
license
Kaur-Parminder Feb 10, 2022
ec6e623
exclude appcontext test
Kaur-Parminder Feb 10, 2022
a611863
Comments and url references
Kaur-Parminder Feb 18, 2022
a4d9de6
review comment
Kaur-Parminder Feb 18, 2022
968183b
fix strbuilder overwrite
Kaur-Parminder Feb 22, 2022
efa3d74
VS Suggestions
Kaur-Parminder Feb 23, 2022
5bb8130
EventSource Trace
Kaur-Parminder Feb 23, 2022
8315240
Documentation and ref
Kaur-Parminder Feb 24, 2022
62d67d9
moved sni native wrapper methods from windows to common
Kaur-Parminder Feb 24, 2022
d126085
reverting back test nuget version
Kaur-Parminder Feb 24, 2022
d1a95ed
Apply suggestions from code review
Kaur-Parminder Feb 24, 2022
b4b0d75
removing body expression from SNIServerEnumClose
Kaur-Parminder Feb 24, 2022
2255f1a
body expression for GetTimeoutSeconds
Kaur-Parminder Feb 24, 2022
ce39cd7
created util for sqldatasourceenumerator
Kaur-Parminder Feb 25, 2022
67f0bbf
more utils
Kaur-Parminder Feb 25, 2022
3f9ad32
license for util file
Kaur-Parminder Feb 25, 2022
d93127c
fix (#2)
DavoudEshtehari Mar 2, 2022
58f8f15
merge
Kaur-Parminder Mar 2, 2022
4ae25f2
changing back encoding to ASCII for SRRP except UDPbroadcast menthod
Kaur-Parminder Mar 2, 2022
3927340
Noimplementation exception for Managed SNI and test update for same
Kaur-Parminder Mar 4, 2022
7aebf45
SNI Update
Kaur-Parminder Mar 7, 2022
e7931b5
Revert "SNI Update"
Kaur-Parminder Mar 7, 2022
855f69f
Merge branch 'dotnet:main' into main
Kaur-Parminder Mar 8, 2022
81e26f6
Merge branch 'main' into MSDEnumerator
Kaur-Parminder Mar 8, 2022
7ff8220
Fix | SqlDSEnumerator net core (#3)
DavoudEshtehari Mar 8, 2022
54b3c36
added test dependcy for ServiceController.dll
Kaur-Parminder Mar 9, 2022
f09becc
review comments
Kaur-Parminder Mar 9, 2022
c0f6bad
Fix build issue
DavoudEshtehari Mar 9, 2022
36d6ade
use TargetsWindows
DavoudEshtehari Mar 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions doc/samples/SqlDataSourceEnumeratorExample.cs
@@ -0,0 +1,33 @@
using System;
//<Snippet1>
using Microsoft.Data.Sql;

class Program
{
static void Main()
{
// Retrieve the enumerator instance and then the data.
SqlDataSourceEnumerator instance =
SqlDataSourceEnumerator.Instance;
System.Data.DataTable table = instance.GetDataSources();

// Display the contents of the table.
DisplayData(table);

Console.WriteLine("Press any key to continue.");
Console.ReadKey();
}

private static void DisplayData(System.Data.DataTable table)
{
foreach (System.Data.DataRow row in table.Rows)
{
foreach (System.Data.DataColumn col in table.Columns)
{
Console.WriteLine("{0} = {1}", col.ColumnName, row[col]);
}
Console.WriteLine("============================");
}
}
}
//</Snippet1>
Kaur-Parminder marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 25 additions & 0 deletions doc/samples/SqlDataSourceEnumeratorVersionExample.cs
@@ -0,0 +1,25 @@
using System;
//<Snippet1>
using Microsoft.Data.Sql;

class Program
{
static void Main()
{
// Retrieve the enumerator instance, and
// then retrieve the data sources.
SqlDataSourceEnumerator instance =
SqlDataSourceEnumerator.Instance;
System.Data.DataTable table = instance.GetDataSources();

// Filter the sources to just show SQL Server 2012 instances.
System.Data.DataRow[] rows = table.Select("Version LIKE '11%'");
foreach (System.Data.DataRow row in rows)
{
Console.WriteLine(row["ServerName"]);
}
Console.WriteLine("Press any key to continue.");
Console.ReadKey();
}
}
//</Snippet1>
65 changes: 65 additions & 0 deletions doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml
@@ -0,0 +1,65 @@
<?xml version="1.0"?>
<docs>
<members name="SqlDataSourceEnumerator">
<SqlDataSourceEnumerator>
<summary>Provides a mechanism for enumerating all available instances of SQL Server within the local network.</summary>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks
SQL Server makes it possible for applications to determine the existence of its instances within the current network. The <xref:Microsoft.Data.Sql.SqlDataSourceEnumerator> class exposes this information to the application developer, providing a <xref:System.Data.DataTable> containing information about all the available servers. This returned table contains a list of server instances that matches the list provided when a user attempts to create a new connection, and on the `Connection Properties` dialog box, expands the drop-down list containing all the available servers.

]]></format>
</remarks>
<related type="Article" href="/dotnet/framework/data/adonet/sql/enumerating-instances-of-sql-server">Enumerating Instances of SQL Server</related>
</SqlDataSourceEnumerator>
<GetDataSources>
<summary>Retrieves a <see cref="T:System.Data.DataTable" /> containing information about all visible SQL Server instances.</summary>
<returns>A <see cref="T:System.Data.DataTable" /> containing information about the visible SQL Server instances.</returns>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks
The table returned by this method contains the following columns, all of which contain strings:

|Column|Description|
|------------|-----------------|
|**ServerName**|Name of the server.|
|**InstanceName**|Name of the server instance. Blank if the server is running as the default instance.|
|**IsClustered**|Indicates whether the server is part of a cluster.|
|**Version**|Version of the server:<br /><br />10.0.xx for SQL Server 2008<br />10.50.x for SQL Server 2008 R2<br />11.0.xx for SQL Server 2012<br />12.0.xx for SQL Server 2014<br />13.0.xx for SQL Server 2016<br />14.0.xx for SQL Server 2017|

> [!NOTE]
> Due to the nature of the mechanism used by <xref:Microsoft.Data.Sql.SqlDataSourceEnumerator> to locate data sources on a network, the method will not always return a complete list of the available servers, and the list might not be the same on every call. If you plan to use this function to let users select a server from a list, make sure that you always also supply an option to type in a name that is not in the list, in case the server enumeration does not return all the available servers. In addition, this method may take a significant amount of time to execute, so be careful about calling it when performance is critical.

## Examples
The following console application retrieves information about all the visible SQL Server instances and displays the information in the console window.

[!code-csharp[SqlDataSourceEnumerator.Example#1](~/../sqlclient/doc/samples/SqlDataSourceEnumeratorExample.cs#1)]

]]></format>
</remarks>
<related type="Article" href="/dotnet/framework/data/adonet/sql/enumerating-instances-of-sql-server">Enumerating Instances of SQL Server</related>
</GetDataSources>
<Instance>
<summary>Gets an instance of the <see cref="T:Microsoft.Data.Sql.SqlDataSourceEnumerator"/>, which can be used to retrieve information about available SQL Server instances.</summary>
<value>An instance of the <see cref="T:Microsoft.Data.Sql.SqlDataSourceEnumerator"/> used to retrieve information about available SQL Server instances.</value>
<remarks>
<format type="text/markdown"><![CDATA[

## Remarks
The <xref:Microsoft.Data.Sql.SqlDataSourceEnumerator> class does not provide a constructor. Use the <xref:Microsoft.Data.Sql.SqlDataSourceEnumerator.Instance%2A> property to retrieve an instance of the class instead.

[!code-csharp[SqlDataSourceEnumeratorExample#1](~/../sqlclient/doc/samples/SqlDataSourceEnumeratorExample.cs#1)]

## Examples
The following console application displays a list of all the available SQL Server 2005 instances within the local network. This code uses the <xref:System.Data.DataTable.Select%2A> method to filter the rows in the table returned by the <xref:Microsoft.Data.Sql.SqlDataSourceEnumerator.GetDataSources%2A> method.
[!code-csharp[SqlDataSourceEnumeratorVersionExample#1](~/../sqlclient/doc/samples/SqlDataSourceEnumeratorVersionExample.cs#1)]

]]></format>
</remarks>
<related type="Article" href="/dotnet/framework/data/adonet/sql/enumerating-instances-of-sql-server">Enumerating Instances of SQL Server</related>

</Instance>
</members>
</docs>
Expand Up @@ -29,6 +29,15 @@ public sealed partial class SqlNotificationRequest
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlNotificationRequest.xml' path='docs/members[@name="SqlNotificationRequest"]/UserData/*' />
public string UserData { get { throw null; } set { } }
}

/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/SqlDataSourceEnumerator/*' />
public sealed class SqlDataSourceEnumerator : System.Data.Common.DbDataSourceEnumerator
{
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/Instance/*' />
public static SqlDataSourceEnumerator Instance { get; }
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/GetDataSources/*' />
public override System.Data.DataTable GetDataSources() { throw null; }
}
}
namespace Microsoft.Data.SqlTypes
{
Expand Down
Expand Up @@ -3,11 +3,15 @@
// See the LICENSE file in the project root for more information.

using Microsoft.Data.SqlClient.SNI;
using System;
using System.Runtime.InteropServices;

namespace Microsoft.Data.SqlClient
{
internal static partial class SNINativeMethodWrapper
{
private const string SNI = "Microsoft.Data.SqlClient.SNI.dll";

internal enum SniSpecialErrors : uint
{
LocalDBErrorCode = SNICommon.LocalDBErrorCode,
Expand Down
Expand Up @@ -12,8 +12,6 @@ namespace Microsoft.Data.SqlClient
{
internal static partial class SNINativeMethodWrapper
{
private const string SNI = "Microsoft.Data.SqlClient.SNI.dll";

private static int s_sniMaxComposedSpnLength = -1;

private const int SniOpenTimeOut = -1; // infinite
Expand Down Expand Up @@ -200,6 +198,7 @@ internal struct SNI_Error
#endregion

#region DLL Imports

[DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIAddProviderWrapper")]
internal static extern uint SNIAddProvider(SNIHandle pConn, ProviderEnum ProvNum, [In] ref uint pInfo);

Expand Down Expand Up @@ -306,7 +305,19 @@ internal struct SNI_Error

[DllImport(SNI, CallingConvention = CallingConvention.Cdecl)]
private static extern uint SNIWriteSyncOverAsync(SNIHandle pConn, [In] SNIPacket pPacket);
#endregion

[DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIServerEnumOpenWrapper")]
internal static extern IntPtr SNIServerEnumOpen();

[DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIServerEnumCloseWrapper")]
internal static extern void SNIServerEnumClose([In] IntPtr packet);

[DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIServerEnumReadWrapper", CharSet = CharSet.Unicode)]
internal static extern int SNIServerEnumRead([In] IntPtr packet,
[In][MarshalAs(UnmanagedType.LPArray)] char[] readBuffer,
[In] int bufferLength,
[MarshalAs(UnmanagedType.Bool)] out bool more);
#endregion

internal static uint SniGetConnectionId(SNIHandle pConn, ref Guid connId)
{
Expand Down
Expand Up @@ -45,6 +45,15 @@
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Common\ActivityCorrelator.cs">
<Link>Microsoft\Data\Common\ActivityCorrelator.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumerator.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumerator.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumeratorManagedHelper.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumeratorManagedHelper.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumeratorUtil.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumeratorUtil.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Common\DbConnectionStringCommon.cs">
<Link>Microsoft\Data\Common\DbConnectionStringCommon.cs</Link>
Expand Down Expand Up @@ -633,6 +642,12 @@
</ItemGroup>
<!-- Windows only -->
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumeratorNativeHelper.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumeratorNativeHelper.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumerator.Windows.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumerator.Windows.cs</Link>
</Compile>
<Compile Include="Microsoft\Data\SqlClient\SqlColumnEncryptionCertificateStoreProvider.Windows.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlColumnEncryptionCngProvider.Windows.cs" />
<Compile Include="Microsoft\Data\SqlClient\SqlColumnEncryptionCspProvider.Windows.cs" />
Expand Down Expand Up @@ -861,6 +876,7 @@
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' != 'true' AND '$(OSGroup)' != 'AnyOS'">
<Compile Include="Microsoft\Data\Sql\SqlDataSourceEnumerator.Unix.cs" />
<Compile Include="Microsoft\Data\ProviderBase\DbConnectionPoolIdentity.Unix.cs" />
<Compile Include="Interop\SNINativeMethodWrapper.Unix.cs" />
<Compile Include="$(CommonPath)\System\Net\Security\NegotiateStreamPal.Unix.cs">
Expand Down
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Data;
using System.Data.Common;
using Microsoft.Data.SqlClient.Server;

namespace Microsoft.Data.Sql
{
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/SqlDataSourceEnumerator/*' />
public sealed partial class SqlDataSourceEnumerator : DbDataSourceEnumerator
{
private partial DataTable GetDataSourcesInternal() => SqlDataSourceEnumeratorManagedHelper.GetDataSources();
}
}
Expand Up @@ -11,10 +11,16 @@

namespace Microsoft.Data.SqlClient.SNI
{
internal class SSRP
internal sealed class SSRP
{
private const char SemicolonSeparator = ';';
private const int SqlServerBrowserPort = 1434;
private const int SqlServerBrowserPort = 1434; //port SQL Server Browser
private const int RecieveMAXTimeoutsForCLNT_BCAST_EX = 15000; //Default max time for response wait
private const int RecieveTimeoutsForCLNT_BCAST_EX = 1000; //subsequent wait time for response after intial wait
private const int ServerResponseHeaderSizeForCLNT_BCAST_EX = 3;//(SVR_RESP + RESP_SIZE) https://docs.microsoft.com/en-us/openspecs/windows_protocols/mc-sqlr/2e1560c9-5097-4023-9f5e-72b9ff1ec3b1
private const int ValidResponseSizeForCLNT_BCAST_EX = 4096; //valid reponse size should be less than 4096
private const int FirstTimeoutForCLNT_BCAST_EX = 5000;//wait for first response for 5 seconds
private const int CLNT_BCAST_EX = 2;//request packet

/// <summary>
/// Finds instance port number for given instance name.
Expand Down Expand Up @@ -149,8 +155,7 @@ private static byte[] SendUDPRequest(string browserHostname, int port, byte[] re
const int sendTimeOutMs = 1000;
const int receiveTimeOutMs = 1000;

IPAddress address = null;
bool isIpAddress = IPAddress.TryParse(browserHostname, out address);
bool isIpAddress = IPAddress.TryParse(browserHostname, out IPAddress address);

byte[] responsePacket = null;
using (UdpClient client = new UdpClient(!isIpAddress ? AddressFamily.InterNetwork : address.AddressFamily))
Expand All @@ -165,9 +170,52 @@ private static byte[] SendUDPRequest(string browserHostname, int port, byte[] re
responsePacket = receiveTask.Result.Buffer;
}
}

return responsePacket;
}
}

/// <summary>
/// Sends request to server, and recieves response from server (SQLBrowser) on port 1434 by UDP
/// Request (https://docs.microsoft.com/en-us/openspecs/windows_protocols/mc-sqlr/a3035afa-c268-4699-b8fd-4f351e5c8e9e)
/// Response (https://docs.microsoft.com/en-us/openspecs/windows_protocols/mc-sqlr/2e1560c9-5097-4023-9f5e-72b9ff1ec3b1)
/// </summary>
/// <returns>string constaning list of SVR_RESP(just RESP_DATA)</returns>
internal static string SendBroadcastUDPRequest()
{
StringBuilder response = new StringBuilder();
byte[] CLNT_BCAST_EX_Request = new byte[1] { CLNT_BCAST_EX }; //0x02
// Waits 5 seconds for the first response and every 1 second up to 15 seconds
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/mc-sqlr/f2640a2d-3beb-464b-a443-f635842ebc3e#Appendix_A_3
int currentTimeOut = FirstTimeoutForCLNT_BCAST_EX;

using (TrySNIEventScope.Create(nameof(SSRP)))
{
using (UdpClient clientListener = new UdpClient())
{
Task<int> sendTask = clientListener.SendAsync(CLNT_BCAST_EX_Request, CLNT_BCAST_EX_Request.Length, new IPEndPoint(IPAddress.Broadcast, SqlServerBrowserPort));
Task<UdpReceiveResult> receiveTask = null;
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SSRP), EventType.INFO, "Waiting for UDP Client to fetch list of instances.");
Stopwatch sw = new Stopwatch(); //for waiting until 15 sec elapsed
sw.Start();
try
{
while ((receiveTask = clientListener.ReceiveAsync()).Wait(currentTimeOut) && sw.ElapsedMilliseconds <= RecieveMAXTimeoutsForCLNT_BCAST_EX && receiveTask != null)
{
currentTimeOut = RecieveTimeoutsForCLNT_BCAST_EX;
SqlClientEventSource.Log.TrySNITraceEvent(nameof(SSRP), EventType.INFO, "Received instnace info from UDP Client.");
if (receiveTask.Result.Buffer.Length < ValidResponseSizeForCLNT_BCAST_EX) //discard invalid response
{
response.Append(Encoding.UTF7.GetString(receiveTask.Result.Buffer, ServerResponseHeaderSizeForCLNT_BCAST_EX, receiveTask.Result.Buffer.Length - ServerResponseHeaderSizeForCLNT_BCAST_EX)); //RESP_DATA(VARIABLE) - 3 (RESP_SIZE + SVR_RESP)
}
}
}
finally
{
sw.Stop();
}
}
}
return response.ToString();
}
}
}
Expand Up @@ -180,5 +180,6 @@ internal static int GetRemainingTimeout(int timeout, long start)
return checked((int)remaining);
}
}
internal static long GetTimeoutSeconds(int timeout) => GetTimeout((long)timeout * 1000L);
}
}
Expand Up @@ -34,6 +34,15 @@ public sealed partial class SqlNotificationRequest
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlNotificationRequest.xml' path='docs/members[@name="SqlNotificationRequest"]/UserData/*' />
public string UserData { get { throw null; } set { } }
}

/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/SqlDataSourceEnumerator/*' />
public sealed class SqlDataSourceEnumerator : System.Data.Common.DbDataSourceEnumerator
{
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/Instance/*' />
public static SqlDataSourceEnumerator Instance {get;}
/// <include file='../../../../doc/snippets/Microsoft.Data.Sql/SqlDataSourceEnumerator.xml' path='docs/members[@name="SqlDataSourceEnumerator"]/GetDataSources/*' />
public override System.Data.DataTable GetDataSources(){ throw null; }
}
}

namespace Microsoft.Data.SqlClient
Expand Down
Expand Up @@ -119,6 +119,18 @@
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlNotificationRequest.cs">
<Link>Microsoft\Data\Sql\SqlNotificationRequest.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumerator.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumerator.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumerator.Windows.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumerator.Windows.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumeratorNativeHelper.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumeratorNativeHelper.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\Sql\SqlDataSourceEnumeratorUtil.cs">
<Link>Microsoft\Data\Sql\SqlDataSourceEnumeratorUtil.cs</Link>
</Compile>
<Compile Include="..\..\src\Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs">
<Link>Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs</Link>
</Compile>
Expand Down