Merge pull request #249 from Azure/andreabatina/netstandard

Updated Elastic Scale Client project to target .Net Standard 2.0 along with .net6.0 and updated sample apps to support AAD auth
This commit is contained in:
Andrea Batina 2024-05-22 15:12:50 +02:00 коммит произвёл GitHub
Родитель d6aeaf9cc5 f5bc10c7fc
Коммит 8838b77f8d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
25 изменённых файлов: 1362 добавлений и 1251 удалений

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase.ElasticScale.Client", "Src\ElasticScale.Client\Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj", "{4C3B3EC4-5702-469E-800E-313FB27A0A2B}"
EndProject
@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests", "Test\ElasticScale.Query.UnitTests\Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests.csproj", "{74CEE77F-D2C7-4B8B-9411-8F97F4E803FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElasticScaleStarterKit", "Samples\ElasticScaleStarterKit\ElasticScaleStarterKit.csproj", "{115A0283-AC42-4D37-97F2-106D168E04D2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticScaleStarterKit", "Samples\ElasticScaleStarterKit\ElasticScaleStarterKit.csproj", "{115A0283-AC42-4D37-97F2-106D168E04D2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityFrameworkMultiTenant", "Samples\EFMultiTenant\EntityFrameworkMultiTenant.csproj", "{BC17F3EF-FEB4-4186-9342-E2D18F1E56AB}"
EndProject
@ -21,6 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticDapper", "Samples\Da
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShardSqlCmd", "Samples\ShardSqlCmd\ShardSqlCmd.csproj", "{B210D6E5-7171-4117-9C77-3F2CB59D04D8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9C78C56E-7738-4D3B-9722-67A4CB028A75}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{A0CBF252-A093-4448-BC10-1A1EF10DB64E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Cover|Any CPU = Cover|Any CPU
@ -143,6 +149,17 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4C3B3EC4-5702-469E-800E-313FB27A0A2B} = {A0CBF252-A093-4448-BC10-1A1EF10DB64E}
{9336E9E7-19BF-49AC-92E3-19FA6B98921E} = {9C78C56E-7738-4D3B-9722-67A4CB028A75}
{BEA6F911-BA98-462C-99AF-3B0595DE2307} = {9C78C56E-7738-4D3B-9722-67A4CB028A75}
{74CEE77F-D2C7-4B8B-9411-8F97F4E803FA} = {9C78C56E-7738-4D3B-9722-67A4CB028A75}
{115A0283-AC42-4D37-97F2-106D168E04D2} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}
{BC17F3EF-FEB4-4186-9342-E2D18F1E56AB} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}
{8EB66613-D5A2-4683-B91A-6AF904FD6B70} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}
{AC76C04B-881E-4CB9-B491-4D19B68459F1} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}
{B210D6E5-7171-4117-9C77-3F2CB59D04D8} = {F7A2F005-3B53-495D-B0EC-CE41FAB4DC75}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {24237160-521B-4CD0-87AB-8994A5E50BE2}
EndGlobalSection

Просмотреть файл

@ -1,15 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace ElasticDapper
{
// Let's use the standard blogging class
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
}
}
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace ElasticDapper
{
// Let's use the standard blogging class
public class Blog
{
public long BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
}
}

Просмотреть файл

@ -5,14 +5,13 @@
</PropertyGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Build.props))\Build.props" />
<ItemGroup>
<PackageReference Include="EnterpriseLibrary.TransientFaultHandling.Data.NetCore" Version="6.0.1312" />
<PackageReference Include="EnterpriseLibrary.TransientFaultHandling.NetCore" Version="6.0.1312" />
<PackageReference Include="Microsoft.Azure.SqlDatabase.ElasticScale.Client" version="2.3.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="DapperExtensions" Version="1.7.0" />
</ItemGroup>
<ItemGroup>
<None Include="LICENSE" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Src\ElasticScale.Client\Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Copyright (c) 2024 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

Просмотреть файл

@ -3,10 +3,10 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using Dapper;
using DapperExtensions;
using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement;
using Microsoft.Data.SqlClient;
////////////////////////////////////////////////////////////////////////////////////////
// This sample illustrates the adjustments that need to be made to use Dapper
@ -28,9 +28,8 @@ namespace ElasticDapper
private static string s_shardmapmgrdb = "[YourShardMapManagerDatabaseName]";
private static string s_shard1 = "[YourShard01DatabaseName]";
private static string s_shard2 = "[YourShard02DatabaseName]";
private static string s_userName = "YourUserName";
private static string s_password = "YourPassword";
private static string s_applicationName = "ESC_Dapv1.0";
private static SqlAuthenticationMethod s_authenticationMethod = SqlAuthenticationMethod.ActiveDirectoryDefault;
// Just two tenants for now.
// Those we will allocate to shards.
@ -39,19 +38,14 @@ namespace ElasticDapper
public static void Main()
{
SqlConnectionStringBuilder connStrBldr = new SqlConnectionStringBuilder
{
UserID = s_userName,
Password = s_password,
ApplicationName = s_applicationName
};
SqlConnectionStringBuilder connStrBldr = GetConnectionStringBuilder();
// Bootstrap the shard map manager, register shards, and store mappings of tenants to shards
// Note that you can keep working with existing shard maps. There is no need to
// re-create and populate the shard map from scratch every time.
Sharding shardingLayer = new Sharding(s_server, s_shardmapmgrdb, connStrBldr.ConnectionString);
shardingLayer.RegisterNewShard(s_server, s_shard1, connStrBldr.ConnectionString, s_tenantId1);
shardingLayer.RegisterNewShard(s_server, s_shard2, connStrBldr.ConnectionString, s_tenantId2);
shardingLayer.RegisterNewShard(s_server, s_shard1, s_tenantId1);
shardingLayer.RegisterNewShard(s_server, s_shard2, s_tenantId2);
// Create schema on each shard.
foreach (string shard in new[] {s_shard1, s_shard2})
@ -67,89 +61,74 @@ namespace ElasticDapper
Console.Write("Enter a name for a new Blog: ");
var name = Console.ReadLine();
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId1,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId1,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
var blog = new Blog { Name = name };
sqlconn.Insert(blog);
}
});
var blog = new Blog { Name = name };
sqlconn.Insert(blog);
}
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId1,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId1,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
// Display all Blogs for tenant 1
IEnumerable<Blog> result = sqlconn.Query<Blog>(@"
SELECT *
FROM Blog
ORDER BY Name");
// Display all Blogs for tenant 1
IEnumerable<Blog> result = sqlconn.Query<Blog>(@"
SELECT *
FROM Blog
ORDER BY Name");
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId1);
foreach (var item in result)
{
Console.WriteLine(item.Name);
}
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId1);
foreach (var item in result)
{
Console.WriteLine(item.Name);
}
});
}
// Do work for tenant 2 :-)
// Here I am going to illustrate how to integrate
// with DapperExtensions which saves us the T-SQL
//
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId2,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId2,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
// Display all Blogs for tenant 2
IEnumerable<Blog> result = sqlconn.GetList<Blog>();
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2);
foreach (var item in result)
{
// Display all Blogs for tenant 2
IEnumerable<Blog> result = sqlconn.GetList<Blog>();
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2);
foreach (var item in result)
{
Console.WriteLine(item.Name);
}
Console.WriteLine(item.Name);
}
});
}
// Create and save a new Blog
Console.Write("Enter a name for a new Blog: ");
var name2 = Console.ReadLine();
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId2,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(
key: s_tenantId2,
connectionString: connStrBldr.ConnectionString,
options: ConnectionOptions.Validate))
{
var blog = new Blog { Name = name2 };
sqlconn.Insert(blog);
}
});
var blog = new Blog { Name = name2 };
sqlconn.Insert(blog);
}
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(s_tenantId2, connStrBldr.ConnectionString, ConnectionOptions.Validate))
{
using (SqlConnection sqlconn = shardingLayer.ShardMap.OpenConnectionForKey(s_tenantId2, connStrBldr.ConnectionString, ConnectionOptions.Validate))
// Display all Blogs for tenant 2
IEnumerable<Blog> result = sqlconn.GetList<Blog>();
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2);
foreach (var item in result)
{
// Display all Blogs for tenant 2
IEnumerable<Blog> result = sqlconn.GetList<Blog>();
Console.WriteLine("All blogs for tenant id {0}:", s_tenantId2);
foreach (var item in result)
{
Console.WriteLine(item.Name);
}
Console.WriteLine(item.Name);
}
});
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
@ -157,14 +136,10 @@ namespace ElasticDapper
private static void CreateSchema(string shardName)
{
SqlConnectionStringBuilder connStrBldr = new SqlConnectionStringBuilder
{
UserID = s_userName,
Password = s_password,
ApplicationName = s_applicationName,
DataSource = s_server,
InitialCatalog = shardName
};
SqlConnectionStringBuilder connStrBldr = GetConnectionStringBuilder();
connStrBldr.DataSource = s_server;
connStrBldr.InitialCatalog = shardName;
using (SqlConnection conn = new SqlConnection(connStrBldr.ToString()))
{
@ -178,5 +153,19 @@ namespace ElasticDapper
)");
}
}
private static SqlConnectionStringBuilder GetConnectionStringBuilder()
{
var connBuilder = new SqlConnectionStringBuilder
{
Authentication = s_authenticationMethod,
ApplicationName = s_applicationName,
CommandTimeout = 60,
ConnectTimeout = 60,
TrustServerCertificate = true
};
return connBuilder;
}
}
}

Просмотреть файл

@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Data.SqlClient;
using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement;
using Microsoft.Data.SqlClient;
namespace ElasticDapper
{
@ -25,41 +25,41 @@ namespace ElasticDapper
ShardMapManager smm;
if (!ShardMapManagerFactory.TryGetSqlShardMapManager(connStrBldr.ConnectionString, ShardMapManagerLoadPolicy.Lazy, out smm))
{
this.ShardMapManager = ShardMapManagerFactory.CreateSqlShardMapManager(connStrBldr.ConnectionString);
ShardMapManager = ShardMapManagerFactory.CreateSqlShardMapManager(connStrBldr.ConnectionString);
}
else
{
this.ShardMapManager = smm;
ShardMapManager = smm;
}
ListShardMap<int> sm;
if (!ShardMapManager.TryGetListShardMap<int>("ElasticScaleWithDapper", out sm))
{
this.ShardMap = ShardMapManager.CreateListShardMap<int>("ElasticScaleWithDapper");
ShardMap = ShardMapManager.CreateListShardMap<int>("ElasticScaleWithDapper");
}
else
{
this.ShardMap = sm;
ShardMap = sm;
}
}
// Enter a new shard - i.e. an empty database - to the shard map, allocate a first tenant to it
public void RegisterNewShard(string server, string database, string connstr, int key)
public void RegisterNewShard(string server, string database, int key)
{
Shard shard;
ShardLocation shardLocation = new ShardLocation(server, database);
if (!this.ShardMap.TryGetShard(shardLocation, out shard))
if (!ShardMap.TryGetShard(shardLocation, out shard))
{
shard = this.ShardMap.CreateShard(shardLocation);
shard = ShardMap.CreateShard(shardLocation);
}
// Register the mapping of the tenant to the shard in the shard map.
// After this step, DDR on the shard map can be used
PointMapping<int> mapping;
if (!this.ShardMap.TryGetMappingForKey(key, out mapping))
if (!ShardMap.TryGetMappingForKey(key, out mapping))
{
this.ShardMap.CreatePointMapping(key, shard);
ShardMap.CreatePointMapping(key, shard);
}
}
}

Просмотреть файл

@ -1,22 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;
namespace ElasticDapper
{
/// <summary>
/// Helper methods for interacting with SQL Databases.
/// </summary>
internal static class SqlDatabaseUtils
{
/// <summary>
/// Gets the retry policy to use for connections to SQL Server.
/// </summary>
public static RetryPolicy SqlRetryPolicy
{
get { return new RetryPolicy<SqlDatabaseTransientErrorDetectionStrategy>(10, TimeSpan.FromSeconds(5)); }
}
}
}

Просмотреть файл

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!--
The SQL server to connect to. Defaults to localdb default instance.
When using Azure SQL DB, use the fully qualified DNS name like "abcdefghij.database.windows.net"
-->
<!-- Azure SQL server: <add key="ServerName" value="your-server-name.database.windows.net" /> -->
<!-- Local SQL server: <add key="ServerName" value="." /> -->
<add key="ServerName" value="." />
<!--
If TrustServerCertificate=true, the connection process skips the trust chain validation.
In this case, the application connects even if the certificate cannot be verified.
Using TrustServerCertificate=false enforces certificate validation and is a best practice.
-->
<add key="TrustServerCertificate" value="true" />
<!--
Set to one of the 10 SqlAuthenticationMethod enum 'Authentication Modes':
https://learn.microsoft.com/dotnet/api/microsoft.data.sqlclient.sqlauthenticationmethod
At time of writing, these are:
ActiveDirectoryDefault
ActiveDirectoryMSI (NOTE: Is the old name of the below)
ActiveDirectoryManagedIdentity (NOTE: Is the new name for MSI (above))
ActiveDirectoryDeviceCodeFlow
ActiveDirectoryServicePrincipal
ActiveDirectoryInteractive
ActiveDirectoryIntegrated (NOTE: behaves as Windows Auth locally)
ActiveDirectoryPassword
SqlPassword
NotSpecified (NOTE: behaves as SqlPassword, i.e. SQL Auth)
-->
<add key="SqlAuthenticationMethod" value="ActiveDirectoryIntegrated" />
<!--
Credentials for connecting to your SQL server if using ActiveDirectoryPassword, SqlPassword (or NotSpecified which is the same as SqlPassword) or ActiveDirectoryServicePrincipal.
- For ActiveDirectoryServicePrincipal the UserName is the Azure Entra 'App Registration' 'Application (client) ID' and the Password is 'Value' of an 'App Registration' 'Secret'.
- For ActiveDirectoryManagedIdentity/ActiveDirectoryMSI, if "user-assigned", the UserName is the Client ID (a GUID) of the Managed Identity (and Password must be "")
-->
<add key="UserName" value="" />
<add key="Password" value="" />
<!--
The database edition to use when creating databases for this sample in Azure SQL DB.
-->
<add key="DatabaseEdition" value="Basic" />
</appSettings>
</configuration>

Просмотреть файл

@ -1,8 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Configuration;
using System.Data.SqlClient;
using Microsoft.Data.SqlClient;
namespace ElasticScaleStarterKit
{
@ -76,17 +77,21 @@ namespace ElasticScaleStarterKit
string userId = ConfigurationManager.AppSettings["UserName"] ?? string.Empty;
string password = ConfigurationManager.AppSettings["Password"] ?? string.Empty;
// Get Integrated Security from the app.config file.
// If it exists, then parse it (throw exception on failure), otherwise default to false.
string integratedSecurityString = ConfigurationManager.AppSettings["IntegratedSecurity"];
bool integratedSecurity = integratedSecurityString != null && bool.Parse(integratedSecurityString);
string trustServerCertificateString = ConfigurationManager.AppSettings["TrustServerCertificate"] ?? string.Empty;
var trustServerCertificate = trustServerCertificateString != null && bool.Parse(trustServerCertificateString);
// Get Sql Auth method from the app.config file.
SqlAuthenticationMethod authMethod;
var enumString = ConfigurationManager.AppSettings["SqlAuthenticationMethod"];
if (!Enum.TryParse(enumString, out authMethod))
{
throw new ArgumentException("Invalid SqlAuthenticationMethod in app.config");
}
SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder
{
// DDR and MSQ require credentials to be set
UserID = userId,
Password = password,
IntegratedSecurity = integratedSecurity,
Authentication = authMethod,
// DataSource and InitialCatalog cannot be set for DDR and MSQ APIs, because these APIs will
// determine the DataSource and InitialCatalog for you.
@ -97,10 +102,52 @@ namespace ElasticScaleStarterKit
//
// Other SqlClient ConnectionString keywords are supported.
TrustServerCertificate = trustServerCertificate,
ApplicationName = "ESC_SKv1.0",
ConnectTimeout = 30
// Set to 120 if ActiveDirectoryDeviceCodeFlow
// not even the fastest cut and pasters can get the device code
// into the browser and click through in 30 seconds.
ConnectTimeout = authMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow ? 120 : 30,
};
// DEVNOTE: NotSpecified behaves the same as SqlPassword (i.e. Sql Auth)
if (authMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity ||
authMethod == SqlAuthenticationMethod.ActiveDirectoryMSI ||
authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal ||
authMethod == SqlAuthenticationMethod.ActiveDirectoryPassword ||
authMethod == SqlAuthenticationMethod.SqlPassword ||
authMethod == SqlAuthenticationMethod.NotSpecified)
{
// DDR and MSQ require credentials to be set
// ActiveDirectoryManagedIdentity / ActiveDirectoryMSI when using a System Managed System Identify does not use a UserID
if (authMethod != SqlAuthenticationMethod.ActiveDirectoryManagedIdentity &&
authMethod != SqlAuthenticationMethod.ActiveDirectoryMSI)
{
if (userId == string.Empty)
{
throw new ArgumentException("UserName must be specified in app.config");
}
}
connStr.UserID = userId;
// ActiveDirectoryManagedIdentity/ActiveDirectoryMSI does not use a Password.
if (authMethod != SqlAuthenticationMethod.ActiveDirectoryManagedIdentity &&
authMethod != SqlAuthenticationMethod.ActiveDirectoryMSI)
{
if (password == string.Empty)
{
throw new ArgumentException("Password must be specified in app.config");
}
connStr.Password = password;
}
}
return connStr.ToString();
}
}
}
}

Просмотреть файл

@ -2,9 +2,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Data.SqlClient;
using System.Linq;
using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement;
using Microsoft.Data.SqlClient;
namespace ElasticScaleStarterKit
{
@ -64,21 +64,23 @@ namespace ElasticScaleStarterKit
// Open and execute the command with retry for transient faults. Note that if the command fails, the connection is closed, so
// the entire block is wrapped in a retry. This means that only one command should be executed per block, since if we had multiple
// commands then the first command may be executed multiple times if later commands fail.
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
// Looks up the key in the shard map and opens a connection to the shard
using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString))
{
// Looks up the key in the shard map and opens a connection to the shard
using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString))
// Create a simple command that will insert or update the customer information
using (SqlCommand cmd = conn.CreateCommand())
{
// Create a simple command that will insert or update the customer information
SqlCommand cmd = conn.CreateCommand();
cmd.RetryLogicProvider = SqlDatabaseUtils.SqlRetryProvider;
cmd.CommandText = @"
IF EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @customerId)
UPDATE Customers
SET Name = @name, RegionId = @regionId
WHERE CustomerId = @customerId
ELSE
INSERT INTO Customers (CustomerId, Name, RegionId)
VALUES (@customerId, @name, @regionId)";
IF EXISTS (SELECT 1 FROM Customers WHERE CustomerId = @customerId)
UPDATE Customers
SET Name = @name, RegionId = @regionId
WHERE CustomerId = @customerId
ELSE
INSERT INTO Customers (CustomerId, Name, RegionId)
VALUES (@customerId, @name, @regionId)";
cmd.Parameters.AddWithValue("@customerId", customerId);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@regionId", regionId);
@ -87,7 +89,7 @@ namespace ElasticScaleStarterKit
// Execute the command
cmd.ExecuteNonQuery();
}
});
}
}
/// <summary>
@ -99,13 +101,13 @@ namespace ElasticScaleStarterKit
int customerId,
int productId)
{
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
// Looks up the key in the shard map and opens a connection to the shard
using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString))
{
// Looks up the key in the shard map and opens a connection to the shard
using (SqlConnection conn = shardMap.OpenConnectionForKey(customerId, credentialsConnectionString))
// Create a simple command that will insert a new order
using (SqlCommand cmd = conn.CreateCommand())
{
// Create a simple command that will insert a new order
SqlCommand cmd = conn.CreateCommand();
cmd.RetryLogicProvider = SqlDatabaseUtils.SqlRetryProvider;
// Create a simple command
cmd.CommandText = @"INSERT INTO dbo.Orders (CustomerId, OrderDate, ProductId)
@ -118,7 +120,7 @@ namespace ElasticScaleStarterKit
// Execute the command
cmd.ExecuteNonQuery();
}
});
}
ConsoleUtils.WriteInfo("Inserted order for customer ID: {0}", customerId);
}
@ -137,4 +139,4 @@ namespace ElasticScaleStarterKit
return s_r.Next(0, maxid);
}
}
}
}

Просмотреть файл

@ -5,9 +5,6 @@
</PropertyGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Build.props))\Build.props" />
<ItemGroup>
<PackageReference Include="EnterpriseLibrary.TransientFaultHandling.Data.NetCore" Version="6.0.1312" />
<PackageReference Include="EnterpriseLibrary.TransientFaultHandling.NetCore" Version="6.0.1312" />
<PackageReference Include="Microsoft.Azure.SqlDatabase.ElasticScale.Client" Version="2.3.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
@ -18,6 +15,11 @@
</None>
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json" />
<ProjectReference Include="..\..\Src\ElasticScale.Client\Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="App.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Copyright (c) 2024 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement;
@ -19,6 +20,8 @@ namespace ElasticScaleStarterKit
Console.WriteLine("*** Welcome to Elastic Database Tools Starter Kit ***");
Console.WriteLine("***********************************************************");
Console.WriteLine();
Console.WriteLine("Authentication method used: {0}", ConfigurationManager.AppSettings["SqlAuthenticationMethod"]);
Console.WriteLine();
// Verify that we can connect to the Sql Database that is specified in App.config settings
if (!SqlDatabaseUtils.TryConnectToSqlDatabase())

Просмотреть файл

@ -1,12 +1,17 @@

Prerequisites:
- Visual Studio 2012 or later, Professional Edition or higher
- Nuget 2.7 or later
- .NET Framework 4.5 or later
- Microsoft Azure SQL Database
Prerequisites
-------------
- .Net 6.0 or later
- Visual Studio 2022
- Microsoft Azure SQL Database or Microsoft SQL Server instance on local machine
Getting Started
---------------
Before running this project, please fill in the values in App.config.
For detailed instructions and background information, please refer to Getting Started web page
for Azure SQL Database Elastic Scale: http://go.microsoft.com/fwlink/?LinkID=510913
Authentication Info
-------------------
https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/azure-active-directory-authentication?view=sql-server-ver16#setting-microsoft-entra-authentication
https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-overview?view=azuresql#connect-by-using-microsoft-entra-identities

Просмотреть файл

@ -3,12 +3,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SqlClient;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;
using Microsoft.Data.SqlClient;
namespace ElasticScaleStarterKit
{
@ -22,6 +20,22 @@ namespace ElasticScaleStarterKit
/// </summary>
public const string MasterDatabaseName = "master";
// Create a retry logic provider
public static SqlRetryLogicBaseProvider SqlRetryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(SqlRetryPolicy);
/// <summary>
/// Gets the retry policy to use for connections to SQL Server.
/// </summary>
private static SqlRetryLogicOption SqlRetryPolicy => new()
{
// Tries 5 times before throwing an exception
NumberOfTries = 5,
// Preferred gap time to delay before retry
DeltaTime = TimeSpan.FromSeconds(1),
// Maximum gap time for each delay time before retry
MaxTimeInterval = TimeSpan.FromSeconds(20)
};
/// <summary>
/// Returns true if we can connect to the database.
/// </summary>
@ -34,11 +48,9 @@ namespace ElasticScaleStarterKit
try
{
using (ReliableSqlConnection conn = new ReliableSqlConnection(
connectionString,
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
}
@ -55,91 +67,96 @@ namespace ElasticScaleStarterKit
public static bool DatabaseExists(string server, string db)
{
using (ReliableSqlConnection conn = new ReliableSqlConnection(
Configuration.GetConnectionString(server, MasterDatabaseName),
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName)))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "select count(*) from sys.databases where name = @dbname";
cmd.Parameters.AddWithValue("@dbname", db);
cmd.CommandTimeout = 60;
int count = conn.ExecuteCommand<int>(cmd);
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.RetryLogicProvider = SqlRetryProvider;
bool exists = count > 0;
return exists;
cmd.CommandText = "select count(*) from sys.databases where name = @dbname";
cmd.Parameters.AddWithValue("@dbname", db);
cmd.CommandTimeout = 60;
int count = (int)cmd.ExecuteScalar();
bool exists = count > 0;
return exists;
}
}
}
public static bool DatabaseIsOnline(string server, string db)
{
using (ReliableSqlConnection conn = new ReliableSqlConnection(
Configuration.GetConnectionString(server, MasterDatabaseName),
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName)))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "select count(*) from sys.databases where name = @dbname and state = 0"; // online
cmd.Parameters.AddWithValue("@dbname", db);
cmd.CommandTimeout = 60;
int count = conn.ExecuteCommand<int>(cmd);
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.RetryLogicProvider = SqlRetryProvider;
bool exists = count > 0;
return exists;
cmd.CommandText = "select count(*) from sys.databases where name = @dbname and state = 0"; // online
cmd.Parameters.AddWithValue("@dbname", db);
cmd.CommandTimeout = 60;
int count = (int)cmd.ExecuteScalar();
bool exists = count > 0;
return exists;
}
}
}
public static void CreateDatabase(string server, string db)
{
ConsoleUtils.WriteInfo("Creating database {0}", db);
using (ReliableSqlConnection conn = new ReliableSqlConnection(
Configuration.GetConnectionString(server, MasterDatabaseName),
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName)))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
SqlCommand cmd = conn.CreateCommand();
// Determine if we are connecting to Azure SQL DB
cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')";
cmd.CommandTimeout = 60;
int engineEdition = conn.ExecuteCommand<int>(cmd);
if (engineEdition == 5)
using (SqlCommand cmd = conn.CreateCommand())
{
// Azure SQL DB
SqlRetryPolicy.ExecuteAction(() =>
{
if (!DatabaseExists(server, db))
{
// Begin creation (which is async for Standard/Premium editions)
cmd.CommandText = string.Format(
"CREATE DATABASE {0} (EDITION = '{1}')",
BracketEscapeName(db),
Configuration.DatabaseEdition);
cmd.CommandTimeout = 60;
cmd.ExecuteNonQuery();
}
});
// Determine if we are connecting to Azure SQL DB
cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')";
cmd.CommandTimeout = 60;
cmd.RetryLogicProvider = SqlRetryProvider;
// Wait for the operation to complete
while (!DatabaseIsOnline(server, db))
int engineEdition = (int)cmd.ExecuteScalar();
if (engineEdition == 5)
{
ConsoleUtils.WriteInfo("Waiting for database {0} to come online...", db);
Thread.Sleep(TimeSpan.FromSeconds(5));
}
// Azure SQL DB
if (!DatabaseExists(server, db))
{
// Begin creation (which is async for Standard/Premium editions)
cmd.CommandText = string.Format(
"CREATE DATABASE {0} (EDITION = '{1}')",
BracketEscapeName(db),
Configuration.DatabaseEdition);
cmd.CommandTimeout = 180;
cmd.ExecuteNonQuery();
}
ConsoleUtils.WriteInfo("Database {0} is online", db);
}
else
{
// Other edition of SQL DB
cmd.CommandText = string.Format("CREATE DATABASE {0}", BracketEscapeName(db));
conn.ExecuteCommand(cmd);
// Wait for the operation to complete
while (!DatabaseIsOnline(server, db))
{
ConsoleUtils.WriteInfo("Waiting for database {0} to come online...", db);
Thread.Sleep(TimeSpan.FromSeconds(5));
}
ConsoleUtils.WriteInfo("Database {0} is online", db);
}
else
{
// Other edition of SQL DB
cmd.CommandText = string.Format("CREATE DATABASE {0}", BracketEscapeName(db));
cmd.ExecuteNonQuery();
}
}
}
}
@ -147,34 +164,33 @@ namespace ElasticScaleStarterKit
public static void DropDatabase(string server, string db)
{
ConsoleUtils.WriteInfo("Dropping database {0}", db);
using (ReliableSqlConnection conn = new ReliableSqlConnection(
Configuration.GetConnectionString(server, MasterDatabaseName),
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, MasterDatabaseName)))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
SqlCommand cmd = conn.CreateCommand();
// Determine if we are connecting to Azure SQL DB
cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')";
cmd.CommandTimeout = 60;
int engineEdition = conn.ExecuteCommand<int>(cmd);
// Drop the database
if (engineEdition == 5)
using (SqlCommand cmd = conn.CreateCommand())
{
// Azure SQL DB
// Determine if we are connecting to Azure SQL DB
cmd.CommandText = "SELECT SERVERPROPERTY('EngineEdition')";
cmd.CommandTimeout = 60;
int engineEdition = (int)cmd.ExecuteScalar();
cmd.CommandText = string.Format("DROP DATABASE {0}", BracketEscapeName(db));
cmd.ExecuteNonQuery();
}
else
{
cmd.CommandText = string.Format(
@"ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE {0}",
BracketEscapeName(db));
cmd.ExecuteNonQuery();
// Drop the database
if (engineEdition == 5)
{
// Azure SQL DB
cmd.CommandText = string.Format("DROP DATABASE {0}", BracketEscapeName(db));
cmd.ExecuteNonQuery();
}
else
{
cmd.CommandText = string.Format(
@"ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE {0}",
BracketEscapeName(db));
cmd.ExecuteNonQuery();
}
}
}
}
@ -182,22 +198,24 @@ namespace ElasticScaleStarterKit
public static void ExecuteSqlScript(string server, string db, string schemaFile)
{
ConsoleUtils.WriteInfo("Executing script {0}", schemaFile);
using (ReliableSqlConnection conn = new ReliableSqlConnection(
Configuration.GetConnectionString(server, db),
SqlRetryPolicy,
SqlRetryPolicy))
using (SqlConnection conn = new SqlConnection(Configuration.GetConnectionString(server, db)))
{
conn.RetryLogicProvider = SqlRetryProvider;
conn.Open();
SqlCommand cmd = conn.CreateCommand();
// Read the commands from the sql script file
IEnumerable<string> commands = ReadSqlScript(schemaFile);
foreach (string command in commands)
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.CommandText = command;
cmd.CommandTimeout = 60;
conn.ExecuteCommand(cmd);
cmd.RetryLogicProvider = SqlRetryProvider;
// Read the commands from the sql script file
IEnumerable<string> commands = ReadSqlScript(schemaFile);
foreach (string command in commands)
{
cmd.CommandText = command;
cmd.CommandTimeout = 60;
cmd.ExecuteNonQuery();
}
}
}
}
@ -233,69 +251,5 @@ namespace ElasticScaleStarterKit
{
return '[' + sqlName.Replace("]", "]]") + ']';
}
/// <summary>
/// Gets the retry policy to use for connections to SQL Server.
/// </summary>
public static RetryPolicy SqlRetryPolicy
{
get
{
return new RetryPolicy<ExtendedSqlDatabaseTransientErrorDetectionStrategy>(10, TimeSpan.FromSeconds(5));
}
}
/// <summary>
/// Extended sql transient error detection strategy that performs additional transient error
/// checks besides the ones done by the enterprise library.
/// </summary>
private class ExtendedSqlDatabaseTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy
{
/// <summary>
/// Enterprise transient error detection strategy.
/// </summary>
private SqlDatabaseTransientErrorDetectionStrategy _sqltransientErrorDetectionStrategy = new SqlDatabaseTransientErrorDetectionStrategy();
/// <summary>
/// Checks with enterprise library's default handler to see if the error is transient, additionally checks
/// for such errors using the code in the in <see cref="IsTransientException"/> function.
/// </summary>
/// <param name="ex">Exception being checked.</param>
/// <returns><c>true</c> if exception is considered transient, <c>false</c> otherwise.</returns>
public bool IsTransient(Exception ex)
{
return _sqltransientErrorDetectionStrategy.IsTransient(ex) || IsTransientException(ex);
}
/// <summary>
/// Detects transient errors not currently considered as transient by the enterprise library's strategy.
/// </summary>
/// <param name="ex">Input exception.</param>
/// <returns><c>true</c> if exception is considered transient, <c>false</c> otherwise.</returns>
private static bool IsTransientException(Exception ex)
{
SqlException se = ex as SqlException;
if (se != null && se.InnerException != null)
{
Win32Exception we = se.InnerException as Win32Exception;
if (we != null)
{
switch (we.NativeErrorCode)
{
case 0x102:
// Transient wait expired error resulting in timeout
return true;
case 0x121:
// Transient semaphore wait expired error resulting in timeout
return true;
}
}
}
return false;
}
}
}
}
}

Просмотреть файл

@ -1,7 +0,0 @@
{
"ServerName": "(localdb)\\v11.0",
"IntegratedSecurity": true,
"UserName": "MyUserName",
"Password": "MyPassword",
"DatabaseEdition": "Basic"
}

Просмотреть файл

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Copyright (c) 2024 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

Просмотреть файл

@ -5,12 +5,12 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Microsoft.Azure.SqlDatabase.ElasticScale.Query;
using Microsoft.Azure.SqlDatabase.ElasticScale.ShardManagement;
using Microsoft.Data.SqlClient;
namespace ShardSqlCmd
{

Просмотреть файл

@ -1,14 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), Build.props))\Build.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Azure.SqlDatabase.ElasticScale.Client" version="2.3.0" />
</ItemGroup>
<ItemGroup>
<None Include="LICENSE" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Src\ElasticScale.Client\Microsoft.Azure.SqlDatabase.ElasticScale.Client.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -5,11 +5,11 @@
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<AssemblyTitle>Microsoft Azure SQL Database: Elastic Database Client Library</AssemblyTitle>
<NeutralLanguage>en-US</NeutralLanguage>
<Version>2.4.1</Version>
<Version>2.4.2-preview1</Version>
<Authors>Microsoft</Authors>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<PackageTags>Microsoft;Elastic;Scale;Azure;SQL;DB;Database;Shard;Sharding;Management;Query;azureofficial</PackageTags>
<PackageReleaseNotes>Updated dependent nugets (due to CVEs), added support for ActiveDirectoryDefault Entra auth</PackageReleaseNotes>
<PackageReleaseNotes>Elastic Scale Client now targets .Net Standard 2.0 along with .Net 6.0 and sample apps are updated to support Entra auth</PackageReleaseNotes>
<PackageIcon>Icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/Azure/elastic-db-tools</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Просмотреть файл

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);SYSLIB0011;</NoWarn>
</PropertyGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('build.props'))" />

Просмотреть файл

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0;net481</TargetFrameworks>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS8073;</NoWarn>
@ -8,7 +8,6 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('build.props'))" />
<Import Project="$([MSBuild]::GetPathOfFileAbove('strongname.props'))" />
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -32,7 +32,7 @@ namespace Microsoft.Azure.SqlDatabase.ElasticScale.Query.UnitTests
/// <summary>
/// User password to use when connecting to shards during a fanout query.
/// </summary>
private static string s_testPassword = "dogmat1C";
private static string s_testPassword = "J8X2ndQTZ8cvu1r";
/// <summary>
/// Table name for the sharded table we will issue fanout queries against.

Просмотреть файл

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>net6.0;net8.0;net481</TargetFrameworks>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<IsPackable>false</IsPackable>
<NoWarn>0649;$(NoWarn)</NoWarn>
@ -8,7 +8,6 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('build.props'))" />
<Import Project="$([MSBuild]::GetPathOfFileAbove('strongname.props'))" />
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />