Introduction
Requires
- Microsoft Visual Studio 2022
- NET 8 Framework
- (SSMS) SQL Server Management Studio
When a developer is the sole developer on a project which interacts with a SQL-Server database stored procedures the developer knows about all the stored procedures but when taking over a project from another developer it can be tedious to check out all the stored procedures in a database or even worst learning about all stored procedures on a server given the developer has proper permission is view the stored procedures.
Rather than having to open (SSMS) SQL Server Management Studio learn how to view stored procedures for an entire server by database with the following Windows Forms utility project along with a console project which shows how to get these stored procedures without a user interface.
For this to be a useful utility project, stored procedure definitions need to be saved off to files with a .sql extension for both syntax coloring when opened by SSMS, VS-Code or Visual Studio, usually the default program is SSMS.
These files are stored under the application folder\Scripts then a folder with today's date.
Usage
Both the console and Windows Forms projects have an appsettings.json file as shown below.
Change Server\Name to the server you want to retrieve stored procedures from.
{ "ConnectionsConfiguration": { "ActiveEnvironment": "Development", "Development": "Data Source=.\\SQLEXPRESS;Initial Catalog=NorthWind2024;Integrated Security=True;Encrypt=False" }, "Server": { "Name": ".\\SQLEXPRESS" } }
Code
Base code resides in two class projects which the console and Windows Forms project share, CommonLibrary and SqlServerLibrary which can be used in a developer’s projects too. Note there some unused code in SqlServerLibrary class project which the reader may find useful for other task.
ConnectionReader class
Provides access to reader the server name from either frontend projects appsettings.json
internal class ConnectionReader { public static string Get(string dbName) { var _configuration = Configuration.JsonRoot(); SqlConnectionStringBuilder builder = new() { DataSource = _configuration.GetValue<string>("Server:Name"), InitialCatalog = dbName, IntegratedSecurity = true, Encrypt = SqlConnectionEncryptOption.Optional }; return builder.ConnectionString; } }
DatabaseService class
Using Dapper to get database names from the SQL-Server instance set in appsettings.json
public class DatabaseService { private readonly IDbConnection _cn = new SqlConnection(ConnectionString()); public async Task<List<string>> DatabaseNames() => ( await _cn.QueryAsync<string>(SqlStatements.GetDatabaseNames)).AsList(); /// <summary> /// Get names of databases on selected server excluding system databases /// </summary> /// <returns></returns> public async Task<List<string>> DatabaseNamesFiltered() => (await _cn.QueryAsync<string>( """ SELECT name FROM sys.databases WHERE name NOT IN ( 'master', 'tempdb', 'model', 'msdb' ) """)) .AsList(); }
SqlStatements class
Contains SQL statements setup as read-only strings which are used in both frontend projects.
public class SqlStatements { /// <summary> /// Provides column names from the description property for each column in a specified table name /// </summary> public static string DescriptionStatement => """ SELECT col.COLUMN_NAME AS ColumnName, col.ORDINAL_POSITION AS Position, ISNULL(prop.value,'(none)') AS [Description] FROM INFORMATION_SCHEMA.TABLES AS tbl INNER JOIN INFORMATION_SCHEMA.COLUMNS AS col ON col.TABLE_NAME = tbl.TABLE_NAME INNER JOIN sys.columns AS sc ON sc.object_id = OBJECT_ID(tbl.TABLE_SCHEMA + '.' + tbl.TABLE_NAME) AND sc.name = col.COLUMN_NAME LEFT JOIN sys.extended_properties prop ON prop.major_id = sc.object_id AND prop.minor_id = sc.column_id AND prop.name = 'MS_Description' WHERE tbl.TABLE_NAME = @TableName ORDER BY col.ORDINAL_POSITION; """; /// <summary> /// Get default values for each column for tables using a connection object /// </summary> public static string GetDefaultValuesInDatabase => """ SELECT SO.[name] AS "TableName", SC.[name] AS "ColumnName", SM.[text] AS "DefaultValue" FROM dbo.sysobjects SO INNER JOIN dbo.syscolumns SC ON SO.id = SC.id LEFT JOIN dbo.syscomments SM ON SC.cdefault = SM.id WHERE SO.xtype = 'U' AND SO.[name] <> 'sysdiagrams' AND SM.[text] IS NOT NULL ORDER BY SO.[name], SC.colid; """; /// <summary> /// Get all database names from master /// </summary> public static string GetDatabaseNames => """ SELECT TableName = DB_NAME(s_mf.database_id) FROM sys.master_files s_mf WHERE s_mf.state = 0 -- ONLINE AND HAS_DBACCESS(DB_NAME(s_mf.database_id)) = 1 AND DB_NAME(s_mf.database_id) NOT IN ( 'master', 'tempdb', 'model', 'msdb' ) AND DB_NAME(s_mf.database_id)NOT LIKE 'ReportServer%' GROUP BY s_mf.database_id ORDER BY 1; """; /// <summary> /// Get details for a table which requires a table name in the calling code /// </summary> public static string TableDetails => """ SELECT c.[name] 'ColumnName', t.[name] 'DataType', c.[max_length] 'MaxLength', c.[precision] 'Precision', c.scale 'Scale', c.is_nullable 'IsNullable', ISNULL(i.is_primary_key, 0) 'PrimaryKey' FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id LEFT OUTER JOIN sys.index_columns ic ON ic.object_id = c.object_id AND ic.column_id = c.column_id LEFT OUTER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id WHERE c.object_id = OBJECT_ID(@TableName); """; /// <summary> /// Get all computed columns in a database using a valid connection object /// </summary> public static string ComputedColumnDefinitions => """ SELECT SCHEMA_NAME(o.schema_id) 'SchemaName', c.name AS 'ColumnName', OBJECT_NAME(c.object_id) AS 'TableName', TYPE_NAME(c.user_type_id) AS 'DataType', c.definition 'Definition' FROM sys.computed_columns c JOIN sys.objects o ON o.object_id = c.object_id ORDER BY SchemaName, TableName, c.column_id; """; /// <summary> /// Determine if table exists in a data using a table name as a parameter /// </summary> public static string TableExists => """ SELECT CASE WHEN EXISTS ( (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName) ) THEN 1 ELSE 0 END; """; /// <summary> /// Get names of user stored procedures that start with usp or usp_ /// </summary> public static string GetUserStoredProcedureNames => """ SELECT [name] FROM sys.procedures WHERE [name] LIKE 'usp%' OR [name] LIKE 'usp_%'; """; public static string WhereInCustomers => """ SELECT C.CustomerIdentifier, C.CompanyName, C.Street, C.City, C.CountryIdentifier, CO.Name AS CountryName, C.Phone, C.ContactId, CT.FirstName, CT.LastName FROM dbo.Customers AS C INNER JOIN dbo.Contacts AS CT ON C.ContactId = CT.ContactId INNER JOIN dbo.Countries AS CO ON C.CountryIdentifier = CO.CountryIdentifier WHERE C.CustomerIdentifier IN ({0}) ORDER BY C.CompanyName """; /// <summary> /// Get all date time columns for tables in database /// </summary> public static string GetAllDateTimeColumnsInDatabase => """ SELECT SCHEMA_NAME(t.schema_id) + '.' + t.name AS [TableName], c.column_id "ColumnId", c.name AS "ColumnName", TYPE_NAME(c.user_type_id) AS DataType, c.scale AS "Scale" FROM sys.columns c JOIN sys.tables t ON t.object_id = c.object_id WHERE TYPE_NAME(c.user_type_id) IN ( 'date', 'datetimeoffset', 'datetime2', 'smalldatetime', 'datetime', 'time' ) ORDER BY [TableName], c.column_id; """; /// <summary> /// Get details for database tables /// Table name, constraint name, primary key column name, foreign table, foreign key column update rule delete rule /// </summary> public static string TableConstraintsForDatabase => """ SELECT PrimaryKeyTable = QUOTENAME(PK.CONSTRAINT_SCHEMA) + '.' + QUOTENAME(PK.TABLE_NAME), ConstraintName = C.CONSTRAINT_NAME, PrimaryKeyColumn = CCU.COLUMN_NAME, ForeignKeyTable = QUOTENAME(FK.CONSTRAINT_SCHEMA) + '.' + QUOTENAME(FK.TABLE_NAME), ForeignKeyColumn = CU.COLUMN_NAME, UpdateRule = C.UPDATE_RULE, DeleteRule = C.DELETE_RULE FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME AND C.CONSTRAINT_CATALOG = FK.CONSTRAINT_CATALOG AND C.CONSTRAINT_SCHEMA = FK.CONSTRAINT_SCHEMA INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME AND C.UNIQUE_CONSTRAINT_CATALOG = PK.CONSTRAINT_CATALOG AND C.UNIQUE_CONSTRAINT_SCHEMA = PK.CONSTRAINT_SCHEMA INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME AND C.CONSTRAINT_CATALOG = CU.CONSTRAINT_CATALOG AND C.CONSTRAINT_SCHEMA = CU.CONSTRAINT_SCHEMA INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CCU ON PK.CONSTRAINT_NAME = CCU.CONSTRAINT_NAME AND PK.CONSTRAINT_CATALOG = CCU.CONSTRAINT_CATALOG AND PK.CONSTRAINT_SCHEMA = CCU.CONSTRAINT_SCHEMA WHERE FK.CONSTRAINT_TYPE = 'FOREIGN KEY' ORDER BY PK.TABLE_NAME, FK.TABLE_NAME """; /// <summary> /// Select for Customers which requires parameters for CustomerIdentifier, PhoneType and ContactType /// </summary> public static string GetCustomers => """ SELECT Cust.CustomerIdentifier, Cust.CompanyName, Cust.City, Cust.PostalCode, C.ContactId, CO.CountryIdentifier, CO.Name AS Country, Cust.Phone, Devices.PhoneTypeIdentifier, Devices.PhoneNumber, Cust.ContactTypeIdentifier, C.FirstName, C.LastName, CT.ContactTitle FROM dbo.Customers AS Cust INNER JOIN dbo.ContactType AS CT ON Cust.ContactTypeIdentifier = CT.ContactTypeIdentifier INNER JOIN dbo.Countries AS CO ON Cust.CountryIdentifier = CO.CountryIdentifier INNER JOIN dbo.Contacts AS C ON Cust.ContactId = C.ContactId INNER JOIN dbo.ContactDevices AS Devices ON C.ContactId = Devices.ContactId WHERE ( Cust.CustomerIdentifier = @CustomerIdentifier AND Devices.PhoneTypeIdentifier = @PhoneType AND Cust.ContactTypeIdentifier = @ContactType ); """; public static string TableNamesForDatabase(string databaseName) => $""" SELECT TABLE_NAME AS TableName FROM [{databaseName}].INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'sysdiagrams' ORDER BY TABLE_NAME; """; public static string DatabaseTablesRowCount => """ SELECT QUOTENAME(SCHEMA_NAME(item.schema_id)) + '.' + QUOTENAME(item.name) AS [Name], SUM(parts.[rows]) AS [RowCount] FROM sys.objects AS item INNER JOIN sys.partitions AS parts ON item.object_id = parts.object_id WHERE item.[type] = 'U' AND item.is_ms_shipped = 0x0 AND parts.index_id < 2 -- 0:Heap, 1:Clustered AND item.[name] <> 'sysdiagrams' GROUP BY item.schema_id, item.[name] ORDER BY [Name]; """; public static string DatabaseTablesRowCount1 => """ SELECT TableSchema = s.name, Name = t.name, [RowCount] = p.rows FROM sys.tables t INNER JOIN sys.schemas s ON t.schema_id = s.schema_id INNER JOIN sys.indexes i ON t.object_id = i.object_id INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id WHERE t.is_ms_shipped = 0 GROUP BY t.name, s.name, p.rows ORDER BY s.name, t.name; """; }
ListDictionary class
This class is used to store database names, table names and user stored procedures. Note the .NET Framework also has a ListDictionary class so be careful when using in other projects to reference this one, not the .NET Framework version.
public class ListDictionary { private Dictionary<string, List<string>> _internalDictionary = new(); public Dictionary<string, List<string>> Dictionary => _internalDictionary; public bool HasItems => _internalDictionary.Count > 0; public void Add(string key, string value) { if (_internalDictionary.TryGetValue(key, out var item)) { if (item.Contains(value) == false) { item.Add(value); } } else { List<string> list = [value]; _internalDictionary.Add(key, list); } } }
Core models
These models are used to stored information for each database user stored procedures
public class DatabaseContainer { public string Database { get; set; } public List<ProcedureContainer> List { get; set; } = new(); public override string ToString() => Database; } public class ProcedureContainer { public string Procedure { get; set; } public string Definition { get; set; } public override string ToString() => Procedure; }
Code cycle through a database
The following code uses the models shown above to stored stored procedures. This code lays the foundation for use in other projects.
public static async Task<List<DatabaseContainer>> GetStoredProcedureDetails() { ListDictionary listDictionary = new(); StoredProcedureHelpers helpers = new(); List<DatabaseContainer> databaseContainers = new(); var service = new DatabaseService(); List<string> dbNames = await service.DatabaseNamesFiltered(); dbNames = dbNames.OrderBy(x => x).ToList(); foreach (var dbName in dbNames) { var (hasStoredProcedures, list) = await helpers .GetStoredProcedureNameSafe(dbName, "'xp_', 'ms_'"); if (hasStoredProcedures) { var root = databaseContainers.FirstOrDefault(x => x.Database == dbName); DatabaseContainer container = new DatabaseContainer { Database = dbName }; if (root is null) { container = new DatabaseContainer { Database = dbName }; } foreach (var item in list) { var definition = await helpers.GetStoredProcedureDefinitionAsync(dbName, item); if (definition is not null && !item.Contains("diagram")) { listDictionary.Add(dbName, item); container.List.Add(new ProcedureContainer { Procedure = item, Definition = definition }); } } databaseContainers.Add(container); } } return databaseContainers.Where(x => x.List.Count > 0).ToList(); }
Windows Form project
There really is nothing to change here other than how user stored procedures are saved if so desire.
In the project file the following creates a Scripts folder beneath the application folder after a build is performed to stored user stored procedures.
<Target Name="MakeScriptsDir" AfterTargets="Build"> <MakeDir Directories="$(OutDir)Scripts\$([System.DateTime]::Now.ToString(yyyy-MM-dd))" /> </Target>
Note
Changing the above requires change the code in the Form SaveButton which expects the folder above to be present.
Special notes
- Anytime a developer writes code in a form avoid best as possible to reference controls. In the code provided a BindingList is used to limit touching controls. One exception which is a no-no is where the code passes the RichTextBox to a class, in this case it was done to limit code in the form which my rule is no more than 120 lines of code.
- Code reuse which is done for events in the form. Always think how can I not duplicate code in a form or class.
Saving stored procedures
Once a database has been selected, click the save button.
- current variable contain everything needed accept the stored procedure definitions. The List property has the stored procedure names.
- Next two variable are used to get the SQL-Server instance nam
- folder variable points to the folder defined in the project file described above.
- The foreach iterates stored procedure names are reads back each stored procedure definition and write each one to a file.
private void SaveButton_Click(object sender, EventArgs e) { var current = _bindingList[DatabaseComboBox.SelectedIndex]; var _configuration = Configuration.JsonRoot(); var serverName = _configuration.GetValue<string>("Server:Name").CleanFileName(); var folder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Scripts", $"{Now.Year}-{Now.Month:d2}-{Now.Day:d2}" ); foreach (var item in current.List) { File.WriteAllText(Path.Combine(folder, $"{serverName}_{current.Name}_{item}.sql"), _helpers.GetStoredProcedureDefinition(DatabaseComboBox.Text, item)); } }
Summary
What has been presented provides an easy way to discover and save off user stored procedures for a SQL-Server instance. Besides being a useful tool just above any level developer can learn new techniques and SQL usage.
Why use stored procedures
Stored procedures can be beneficial in C# because they can improve application performance, make code easier to maintain, and encapsulate complex logic in the database. They can also be used to perform a variety of tasks, including retrieving, updating, and deleting data.
Stored procedures are not vulnerable to SQL Injection attacks and are more secure than dynamic SQL when multiple applications access the database.
Should I use stored procedures?
Do the research and decide for yourself.
Support for stored procedures
- All data providers
- EF Core
- Dapper
Top comments (0)