Exploring GitHub Copilot with Microsoft Visual Studio

Exploring GitHub Copilot with Microsoft Visual Studio

Introduction

Learn how to use GitHub Copilot in Microsoft Visual Studio 2022 with real world usage from documenting code, improving code, explain code and more. Also, will talk about some features of Copilot in Microsoft Visual Studio Code.

Ways to use GitHub Copilot

  • Write pull request summaries (Copilot Enterprise feature only)
  • Generate commit messages
  • Fix code inline
  • Generate documentation for your code
  • Meaningful names matter (VS Code F2)
  • Create unit test
  • Assist with debugging code
  • Explaining code
  • Documenting code

The following table was copied from this page in raw format then asked Copilot to create a three column markdown table 💚

Command Description Usage
/explain Get code explanations Open file with code or highlight code you want explained and type: /explain what is the fetchPrediction method?
/fix Receive a proposed fix for the problems in the selected code Highlight problematic code and type: /fix propose a fix for the problems in fetchAirports route
/tests Generate unit tests for selected code Open file with code or highlight code you want tests for and type: /tests
/help Get help on using Copilot Chat Type: /help what can you do?
/clear Clear current conversation Type: /clear
/doc Add a documentation comment Highlight code and type: /doc
/generate Generate code to answer your question Type: /generate code that validates a phone number
/optimize Analyze and improve running time of the selected code Highlight code and type: /optimize fetchPrediction method
/new Scaffold code for a new workspace Type: /new create a new django app
/simplify Simplify the selected code Highlight code and type: /simplify
/feedback Provide feedback to the team Type: /feedback

Documenting code

Developers here tend not to document code which in some cases means that other developers may not understand the purpose of a class or method. Even when a method name is meaningful the method should be documented for several reasons, first, when a code base will have help generated from XML documentation and for clarification of usage and meaning of parameters and return types are a few reasons to document code.

Example 1

Here is a method which is easy to understand.

public static class DateTimeExtensions
{ public static DateOnly FirstDateOfWeek(this DateTime sender, DayOfWeek startOfWeek = DayOfWeek.Sunday) => DateOnly.FromDateTime(sender.AddDays(-1 * (7 + (sender.DayOfWeek - startOfWeek)) % 7));
}
Enter fullscreen mode
Exit fullscreen mode

When using the above method this is what is shown with Intellisense.

shown with Intellisense

Using /doc feature of GitHub Copilot

public static class DateTimeExtensions
{ /// <summary> /// Calculates the first date of the week based on a given start day of the week. /// </summary> /// <param name="sender">The DateTime object representing the current date.</param> /// <param name="startOfWeek">The start day of the week (default is Sunday).</param> /// <returns>The first date of the week.</returns> public static DateOnly FirstDateOfWeek(this DateTime sender, DayOfWeek startOfWeek = DayOfWeek.Sunday) => DateOnly.FromDateTime(sender.AddDays(-1 * (7 + (sender.DayOfWeek - startOfWeek)) % 7));
}
Enter fullscreen mode
Exit fullscreen mode

Much better with proper documentation from Copilot.

proper documentation from Copilot

Example 2

A developer comes across the following method has been asked to document the method.

public partial class Helpers
{ public static string NextValue(string sender, int incrementBy = 1) { string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0'); } [GeneratedRegex("[0-9]+$")] private static partial Regex NumbersPattern();
}
Enter fullscreen mode
Exit fullscreen mode

To document the code.

  1. Select the method
  2. Press ALT + /
  3. Type / which provides options, select doc
  4. Press ENTER

Results

/// <summary>
/// Generates the next value based on the given sender string and incrementBy value.
/// </summary>
/// <param name="sender">The sender string.</param>
/// <param name="incrementBy">The value to increment by (default is 1).</param>
/// <returns>The next value.</returns>
public static string NextValue(string sender, int incrementBy = 1)
{ string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0');
}
Enter fullscreen mode
Exit fullscreen mode

Next, jumping ahead to explaining code which will be covered later. A developer is curious how the above code works.

string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0');
Enter fullscreen mode
Exit fullscreen mode
  1. Select the following body of code.
  2. Press ALT + /
  3. Press /, select explain
  4. Press ENTER

The following is provided and at the bottom additional help.

Helpful information at bottom of explain dialog

The selected code is a part of the NextValue method in the Helpers class. Let’s break it down step by step:

  1. string value = NumbersPattern().Match(sender).Value;
    • This line retrieves the numeric value from the sender string using a regular expression pattern defined in the NumbersPattern method.
    • The NumbersPattern method is annotated with the [GeneratedRegex(“[0-9]+$”)] attribute, which indicates that it generates a regular expression pattern that matches one or more digits at the end of a string.
    • The Match method is called on the result of NumbersPattern() to find the first occurrence of the pattern in the sender string.
    • The Value property of the Match result is assigned to the value variable.
  2. return sender[..^value.Length] + (long.Parse(value) + incrementBy).ToString().PadLeft(value.Length, ‘0’);
  3. This line constructs and returns the next value based on the sender string and the incrementBy value.
  4. The sender[..^value.Length] expression is used to remove the numeric value from the end of the sender string. It uses the range operator .. to specify a range from the beginning of the string to the length of the sender string minus the length of the value string.
  5. (long.Parse(value) + incrementBy) parses the value string as a long integer and adds the incrementBy value to it.
  6. .ToString().PadLeft(value.Length, ‘0’) converts the sum to a string and pads it with leading zeros to match the length of the original value string.
  7. The resulting string is concatenated with the modified sender string and returned as the next value.
    In summary, the selected code extracts a numeric value from the sender string using a regular expression pattern, and then constructs the next value by incrementing the extracted value by the specified incrementBy value and padding it with leading zeros.

With both example code is document and in the last code has been broken down to understand code and in some cases assist if code needs modifications.

Using Copilot to explain code

Above an example for C# was presented for explaining code, Copilot can also explain JavaScript.

A developer wants to understand the following code is for a button click event but does not understand the code and would like to understand the code.

document.getElementById('getButton').addEventListener('click', function () { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://jsonplaceholder.typicode.com/users"); xhr.send(); xhr.responseType = "json"; xhr.onload = () => { if (xhr.readyState === 4 && xhr.status === 200) { const data = xhr.response; document.getElementById("people-template").innerHTML = ""; console.table(data, [ "id", "name", "email" ]); $('#people-template').append(` ${data.reduce((updated, user) => updated.concat(`<li>${user.id} <strong>Name</strong> ${user.name.padEnd(35, '.')} ${user.email}</li>`), '')}`); } else { console.log(`Error: ${xhr.status}`); } };
});
Enter fullscreen mode
Exit fullscreen mode

Using /explain the following is provided.

The selected code is an event listener that triggers when the “Get” button is clicked. It performs an AJAX GET request to retrieve data from the “https://jsonplaceholder.typicode.com/users” URL. Let’s break down the code step by step:

  1. document.getElementById(‘getButton’).addEventListener(‘click’, function () { … });: This line adds an event listener to the “Get” button. When the button is clicked, the function inside the event listener will be executed.
  2. const xhr = new XMLHttpRequest();: This line creates a new XMLHttpRequest object. The XMLHttpRequest object is used to interact with servers and make HTTP requests.
  3. xhr.open(“GET”, “https://jsonplaceholder.typicode.com/users”);: This line initializes the request by specifying the HTTP method (GET) and the URL from which to retrieve data.
  4. xhr.send();: This line sends the HTTP request to the server.
  5. xhr.responseType = “json”;: This line sets the expected response type to JSON. This tells the browser to automatically parse the response as JSON.
  6. xhr.onload = () => { … };: This line sets up a callback function to handle the response when it is received. The function will be executed when the request is complete and the response is available.
  7. if (xhr.readyState === 4 && xhr.status === 200) { … }: This line checks if the request is complete (readyState 4) and if the response status is 200 (indicating a successful request).
  8. const data = xhr.response;: This line assigns the response data to the data variable.
  9. document.getElementById(“people-template”).innerHTML = “”;: This line clears the existing content of the “people-template” element.
  10. console.table(data, [“id”, “name”, “email”]);: This line logs the data in a table format to the console, displaying only the “id”, “name”, and “email” properties.
  11. $(‘#people-template’).append( … );: This line uses jQuery to append HTML content to the “people-template” element. It uses the data.reduce() method to iterate over the data array and generate a list item (
  12. ) for each user. The user’s ID, name, and email are displayed in the list item.
  13. console.log(Error: ${xhr.status});: If the request is not successful (status code other than 200), this line logs an error message to the console, including the status code.
    In summary, when the “Get” button is clicked, the code sends an AJAX GET request to retrieve user data from a remote server. If the request is successful, the data is displayed in a table format and appended to the “people-template” element as a list. If there is an error, an error message is logged to the console.

What about CSS?

A great many developers in the world do not have a grasp of working with CSS. With that in mind a developer wants to better understand the following rule.

legend { background: #0f506f; border: solid 1px black; -webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; padding: 6px; color: white;
}
Enter fullscreen mode
Exit fullscreen mode

Use /explain and unlike the prior examples done in Visual Studio 2022, this one is done in VS Code.

Results from Copilot

Results from Copilot

What about SQL?

A developer comes across the following and wants to understand the statement.

WITH CTE
AS (SELECT t2.id, t2.TransDate AS [Date], t2.Credit, t2.Debit, SUM(COALESCE(t1.credit, 0) - COALESCE(t1.debit, 0)) AS Balance FROM dbo.Transactions t1 INNER JOIN dbo.Transactions t2 ON t1.TransDate <= t2.TransDate GROUP BY t2.TransDate, t2.Credit, t2.Debit, t2.id)
SELECT id, [Date], Credit, Debit
FROM CTE
ORDER BY CTE.[Date];
Enter fullscreen mode
Exit fullscreen mode
  1. Select the statment
  2. Press ALT + /
  3. ENTER
  4. /explain
  5. Press ENTER

Copilot provides a detailed explanation along with a suggestion to modify the statement yet in this case is guessing and the guess is wrong but may open an idea up to a developer.

suggestion from above

What about documenting the statement.

  1. Select the statment
  2. Press ALT + /
  3. ENTER
  4. /doc
  5. Press ENTER
/* The following query uses a common table expression (CTE) to calculate the balance of transactions over time. It retrieves the transaction ID, date, credit amount, and debit amount from the Transactions table. The CTE calculates the balance by summing the difference between the credit and debit amounts for each transaction. The result is ordered by date in ascending order.
*/ WITH CTE
AS (SELECT t2.id, t2.TransDate AS [Date], t2.Credit, t2.Debit, SUM(COALESCE(t1.credit, 0) - COALESCE(t1.debit, 0)) AS Balance FROM dbo.Transactions t1 INNER JOIN dbo.Transactions t2 ON t1.TransDate <= t2.TransDate GROUP BY t2.TransDate, t2.Credit, t2.Debit, t2.id)
SELECT id, [Date], Credit, Debit
FROM CTE
ORDER BY CTE.[Date];
Enter fullscreen mode
Exit fullscreen mode

Ask Copilot to refactor your code

To start off, general refactoring can be changing a method with a statement body to an expression body member using Visual Studio ReSharper extension.

Using statement body

public string[] Items()
{ return new[] { ApplicationName, ApplicationVersion, ApplicationKey };
}
Enter fullscreen mode
Exit fullscreen mode

Refactor to expression body

public string[] Items() => new[] { ApplicationName, ApplicationVersion, ApplicationKey };
Enter fullscreen mode
Exit fullscreen mode

Improve/refactor code with Copilot

Given the following method used for decades to return a list of primary keys from a database table a developer learns about Dapper and wants to rewrite this code using Dapper.

public List<int> CategoryIdentifiers()
{ var idList = new List<int>(); var selectStatement = "SELECT DISTINCT CategoryID FROM dbo.Categories "; using (var cn = new SqlConnection() { ConnectionString = ConnectionString }) { using (var cmd = new SqlCommand() { Connection = cn }) { cmd.CommandText = selectStatement; cn.Open(); var reader = cmd.ExecuteReader(); while (reader.Read()) { idList.Add(reader.GetInt32(0)); } } } return idList;
}
Enter fullscreen mode
Exit fullscreen mode
  1. Select the above method
  2. Press ALT + /
  3. Enter Refactor this method to use Dapper.
  4. ENTER

The following is produced.

public List<int> CategoryIdentifiers()
{ var idList = new List<int>(); var selectStatement = "SELECT DISTINCT CategoryID FROM dbo.Categories"; using (var cn = new SqlConnection() { ConnectionString = ConnectionString }) { cn.Open(); idList = cn.Query<int>(selectStatement).AsList(); } return idList;
}
Enter fullscreen mode
Exit fullscreen mode

Copilot did a great job on refactoring and works for inexperienced developers but an experienced developer can farther modify the code.

Here the experienced developer understands that Dapper will open the connection for us and the SQL statement does not really need to be formatted but imagine a large complex SQL statement with many joins, this would make a difference.

public List<int> CategoryIdentifiers()
{ using SqlConnection cn = new() { ConnectionString = ConnectionString }; return cn.Query<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """).AsList();
}
Enter fullscreen mode
Exit fullscreen mode

Let’s now ask Copilot to make the above code asynchronous.

Copilot produces the following yet appears to change the SQL from a raw string literal to a verbatim string.

public async Task<List<int>> CategoryIdentifiersAsync()
{ await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>(@" SELECT DISTINCT CategoryID FROM dbo.Categories ")).AsList();
}
Enter fullscreen mode
Exit fullscreen mode

ReSharper will provide a refactor recommendation and the developer accepts the recommendation.

public async Task<List<int>> CategoryIdentifiersAsync()
{ await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList();
}
Enter fullscreen mode
Exit fullscreen mode

Ask Copilot to write a unit test

  1. Select the above method
  2. Press ALT + /
  3. Enter write a unit test
  4. ENTER

Note
We get a standard MS-Test but could specify which test framework to use.

[TestClass]
public class DapperOperationsTests
{ [TestMethod] public async Task CategoryIdentifiersAsync_ShouldReturnDistinctCategoryIDs() { // Arrange var dapperOperations = new DapperOperations(); // Act var result = await dapperOperations.CategoryIdentifiersAsync(); // Assert Assert.IsNotNull(result); CollectionAssert.AllItemsAreUnique(result); }
}
Enter fullscreen mode
Exit fullscreen mode

Ask Copilot to add error handling

Add error handling with SeriLog logging to this method

  1. Select the above method
  2. Press ALT + /
  3. Enter add error handling with SeriLog logging to this method
  4. ENTER

The results were not what the developer wanted.

public async Task<List<int>> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>(@" SELECT DISTINCT CategoryID FROM dbo.Categories ")).AsList(); } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); throw; }
}
Enter fullscreen mode
Exit fullscreen mode
  • We are back to a verbatim string, use ReSharper to use a raw string literal.
public async Task<List<int>> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList();
 } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); throw; }
}
Enter fullscreen mode
Exit fullscreen mode

Let now go on without Copilot and get rid of throw and change the return type to a tuple which can be deconstructed by the caller.

public async Task<(bool, List<int>)> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (true,(await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList());
 } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); return (false, null); }
}
Enter fullscreen mode
Exit fullscreen mode

Unit test revision

Now that there has been a change to the return type ask Copilot to write a new unit test.

[TestClass]
public class DapperOperationsTests
{ [TestMethod] public async Task CategoryIdentifiersAsync_ShouldReturnCategoryIDs() { // Arrange DapperOperations dapperOperations = new DapperOperations(); // Act (bool success, List<int> categoryIds) = await dapperOperations.CategoryIdentifiersAsync(); // Assert Assert.IsTrue(success); Assert.IsNotNull(categoryIds); Assert.IsTrue(categoryIds.Count > 0); }
}
Enter fullscreen mode
Exit fullscreen mode

Unit test

In each example for unit test only one test was written, a developer should do a mirror image of a good test and have a one or more test for when things break.

Asking Copilot to write new code

Copilot can write code, for instance a developer needs a string language extension method that splits a string as follows, FirstName to First Name. The developer finds the following language extension but thinks there may be a more efficient way and not using regular expressions.

public static class StringExtensions
{ private static readonly Regex CamelCaseRegex = new(@"([A-Z][a-z]+)"); /// <summary> /// KarenPayne => Karen Payne /// </summary> [DebuggerStepThrough] public static string SplitCamelCase(this string sender) => string.Join(" ", CamelCaseRegex.Matches(sender) .Select(m => m.Value));
}
Enter fullscreen mode
Exit fullscreen mode

They ask several questions, but none are sufficient but than decide to include using span and the following is provided.

public static class StringExtensions
{ /// <summary> /// Split text at each capital letter /// </summary> /// <param name="input">string to work on</param> /// <returns> /// <para>An empty string, if the input is null or empty.</para> /// <para>Same as original if nothing affected</para> /// <para>String split on each uppercase token</para> /// <para>SSMS would become S S M S</para> /// </returns> [DebuggerStepThrough] public static string SplitCamelCase(this string input) { if (string.IsNullOrEmpty(input)) { return input; } Span<char> result = stackalloc char[input.Length * 2]; var resultIndex = 0; for (var index = 0; index < input.Length; index++) { var currentChar = input[index]; if (index > 0 && char.IsUpper(currentChar)) { result[resultIndex++] = ' '; } result[resultIndex++] = currentChar; } return result[..resultIndex].ToString(); }
}
Enter fullscreen mode
Exit fullscreen mode

Note
Of course, some developers favor shorter code as in the first code written yet shorter code is not always the best so if curious run both through Benchmarks.

Interface and classes

The following is a very simple example for using AI to create interfaces and classes in C# using GitHub Copilot and JetBrains ReSharper.

Note
Jetbrains ReSharper is a paid extension for Visual Studio. It is used here to move the generated code to proper folders and update the namespaces. Also, Jetbrains has a paid AI service which Karen has a ticket for.

Here we want to create

  • A generic interface
  • Customer class to implement above interface
  • Both of the above get created in Program.cs using Copilot.

✔️ The interface are then moved using Jetbrain’s ReSharper Visual Studio extension to proper folders.

✔️ During the ReSharper action Visual Studio kicks in and asked to update the namespace.

A console project is used for this experiment in Program.cs.

Question 1
Create a public crud interface named IBase under Program.cs

Copilot responds with the following.

namespace InterfacesClassesExamples
{ public interface IBase<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(T entity); } internal partial class Program { static void Main(string[] args) { } }
}
Enter fullscreen mode
Exit fullscreen mode

Question 2
Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property.

Copilot responds with the following.

namespace InterfacesClassesExamples
{ public interface IBase<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(T entity); } public class Customer : IBase<Customer>, INotifyPropertyChanged { private int _id; private string _firstName; private string _lastName; private string _email; private DateOnly _birthDate; public int Id { get { return _id; } set { if (_id != value) { _id = value; OnPropertyChanged(nameof(Id)); } } } public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } public Customer GetById(int id) { // Implementation of GetById method throw new NotImplementedException(); } public IEnumerable<Customer> GetAll() { // Implementation of GetAll method throw new NotImplementedException(); } public void Add(Customer entity) { // Implementation of Add method throw new NotImplementedException(); } public void Update(Customer entity) { // Implementation of Update method throw new NotImplementedException(); } public void Delete(Customer entity) { // Implementation of Delete method throw new NotImplementedException(); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } internal partial class Program { static void Main(string[] args) { } }
}
Enter fullscreen mode
Exit fullscreen mode

Next steps, create an Interface and Models folder and move IBase to the Interface folder and Customer to the Model folder or copy IBase and Customer to appropriate folders in another project. ReSharper can assist with moving IBase and Customer to their own files and Visual Studio will assist with setting up proper namespacing.

Now suppose the above was done in the same console project but in a class outside of Program.cs the results vary.

In one case the following

public class Customer : IBase<Customer>, INotifyPropertyChanged
Enter fullscreen mode
Exit fullscreen mode

Is created as

public class Customer : IBase<int>, INotifyPropertyChanged
Enter fullscreen mode
Exit fullscreen mode

In another test in a separate class the following question was posed to Copilot.

Create a public crud generic interface named IBase with a constraint on T. create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property

The response was missing private fields for property change notification and IBase was not implemented and duplicate property names.

namespace InterfacesClassesExamples.Test
{ public interface IBase<T> where T : class { void Create(T entity); T Read(int id); void Update(T entity); void Delete(int id); } public class Customer : IBase<Customer>, INotifyPropertyChanged { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateOnly BirthDate { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // Implement INotifyPropertyChanged for each property public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } // Implement INotifyPropertyChanged for each property public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } // Implement INotifyPropertyChanged for each property public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } // Implement INotifyPropertyChanged for each property public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } }
}
Enter fullscreen mode
Exit fullscreen mode

Next, still in the same class as above.

  1. Create a public crud interface named IBase
  2. Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property.

Results with no change property notification code.

namespace InterfacesClassesExamples.Test
{ public interface IBase<T> where T : class { T GetById(int id); void Add(T entity); void Update(T entity); void Delete(T entity); } public class Customer : IBase<Customer>, INotifyPropertyChanged { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateOnly BirthDate { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public Customer GetById(int id) { // Implementation of GetById method } public void Add(Customer entity) { // Implementation of Add method } public void Update(Customer entity) { // Implementation of Update method } public void Delete(Customer entity) { // Implementation of Delete method } }
}
Enter fullscreen mode
Exit fullscreen mode

Next, here the experiment was done in a new class project.

Ask Copilot the same questions as in prior failures and failed again.

Instead of

public class Customer : IBase<Customer>, INotifyPropertyChanged
Enter fullscreen mode
Exit fullscreen mode

Copilot produced.

public class Customer : IBase, INotifyPropertyChanged
Enter fullscreen mode
Exit fullscreen mode

Then when ready to try again the following was suggested without property change notification.

copilot suggestion without asking

Wait a minute

Let’s try the last question as follows.

_Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property
_

And we get. Next step, move Customer to it’s own file.

using System.ComponentModel; namespace InterfacesClassesExamplesLibrary.Interfaces
{ public class Customer : IBase<Customer>, INotifyPropertyChanged { private int _id; private string _firstName; private string _lastName; private string _email; private DateOnly _birthDate; public int Id { get { return _id; } set { if (_id != value) { _id = value; OnPropertyChanged(nameof(Id)); } } } public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } public Customer GetById(int id) { // Implementation of GetById method } public void Insert(Customer entity) { // Implementation of Insert method } public void Update(Customer entity) { // Implementation of Update method } public void Delete(Customer entity) { // Implementation of Delete method } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public interface IBase<T> { T GetById(int id); void Insert(T entity); void Update(T entity); void Delete(T entity); }
}
Enter fullscreen mode
Exit fullscreen mode

Lessons learn for generating new code

  • Location of where the question is asked determines how copilot will respond and how the question is asked will also determine copilot’s response.
  • Sometimes it is faster to just write the code by hand if responding solutions are not to a developers requirements.

Tip

First out questions in a text file, ask Copilot, if the response is not proper, go back and create a copy of the question in the text file and rephrase. Try until a good enough response is provided or go old school and write code yourself.

Debugging

When GitHub Copilot is active and a runtime exception is raised a developer can click, Ask Copilot to get suggestions which is what the project is for. Once asking coilot a window opens with recommendations and in most cases why the exception was thrown.

The connection string is deliberately setup to point to a non-existing SQL-Server instance and the default time out has been altered from the original which is 30 seconds down to two seconds.

Ask copilot while debugging

Note
This project was created to show a simple example for SQL-Server computed columns and was put here to show the above.

About (When the project has no issues)

Provides an interesting way to compute how old a person is in years.

Original code came from this Stackoverflow post.

I took the code and created a simple Console project to demonstrate how to use it with a computed column in a SQL-Server database table using Dapper to read the data.

CREATE TABLE [dbo].[BirthDays]( [Id] [int] IDENTITY(1,1) NOT NULL, [FirstName] [nvarchar](max) NULL, [LastName] [nvarchar](max) NULL, [BirthDate] [date] NULL, [YearsOld] AS ((CONVERT([int],format(getdate(),'yyyyMMdd'))-CONVERT([int],format([BirthDate],'yyyyMMdd')))/(10000))
Enter fullscreen mode
Exit fullscreen mode
  • Take birthdate and current date, format and convert to integer
  • Subtract birthdate from current date
  • Divide by 10,000 to get years old

Source code

Do not expect the code provided to be stable as it was used for demonstrating what has been provided above.

Source code

VS Code

Using GitHub Copilot works great here as well as Visual Studio.

The shortcut is CTRL + I rather than ALT + / in Visual Studio.

A great usage is fixing code, in the screenshot below there are errors on each input because they are not associated with labels. Hover and a menu appears, use the last menu item to see why this is an issue (a WCAG AA rule) and second from bottom to fix.

VS Code showing part of an html file

Let’s fix the issue with copilot by associating labels above inputs with the proper input and for kicks, surround the labels and inputs with a fieldset.

Press CTRL + I and ask with

wrap in fieldset and legend then associate inputs with labels

before and after asking copilot

Copilot History

In VS Code, use the arrow keys to traverse history for the current session while Visual Studio does not have history available.

Security

Use caution to what text is placed into your Copilot questions as they very well be seen by other users of Copilot. In one case while writing documentation into a markdown file Copilot suggested text that was from a closed system. They were informed that this happened. Imagine exposing customer information or internal server details and a bad actor got the information what might happen?

Summary

Made it this far? That means by applying what was written the reader will have a better expectation of Copilot. Copilot is a great edition to programmers at every level but one should not rely solely on Copilot as seen not all response will be correct. As Copilot matures expect better responses and more features.


Discover more from Coursity

Subscribe to get the latest posts to your email.

Exploring GitHub Copilot with Microsoft Visual Studio

Exploring GitHub Copilot with Microsoft Visual Studio

Introduction

Learn how to use GitHub Copilot in Microsoft Visual Studio 2022 with real world usage from documenting code, improving code, explain code and more. Also, will talk about some features of Copilot in Microsoft Visual Studio Code.

Ways to use GitHub Copilot

  • Write pull request summaries (Copilot Enterprise feature only)
  • Generate commit messages
  • Fix code inline
  • Generate documentation for your code
  • Meaningful names matter (VS Code F2)
  • Create unit test
  • Assist with debugging code
  • Explaining code
  • Documenting code

The following table was copied from this page in raw format then asked Copilot to create a three column markdown table 💚

Command Description Usage
/explain Get code explanations Open file with code or highlight code you want explained and type: /explain what is the fetchPrediction method?
/fix Receive a proposed fix for the problems in the selected code Highlight problematic code and type: /fix propose a fix for the problems in fetchAirports route
/tests Generate unit tests for selected code Open file with code or highlight code you want tests for and type: /tests
/help Get help on using Copilot Chat Type: /help what can you do?
/clear Clear current conversation Type: /clear
/doc Add a documentation comment Highlight code and type: /doc
/generate Generate code to answer your question Type: /generate code that validates a phone number
/optimize Analyze and improve running time of the selected code Highlight code and type: /optimize fetchPrediction method
/new Scaffold code for a new workspace Type: /new create a new django app
/simplify Simplify the selected code Highlight code and type: /simplify
/feedback Provide feedback to the team Type: /feedback

Documenting code

Developers here tend not to document code which in some cases means that other developers may not understand the purpose of a class or method. Even when a method name is meaningful the method should be documented for several reasons, first, when a code base will have help generated from XML documentation and for clarification of usage and meaning of parameters and return types are a few reasons to document code.

Example 1

Here is a method which is easy to understand.

public static class DateTimeExtensions
{ public static DateOnly FirstDateOfWeek(this DateTime sender, DayOfWeek startOfWeek = DayOfWeek.Sunday) => DateOnly.FromDateTime(sender.AddDays(-1 * (7 + (sender.DayOfWeek - startOfWeek)) % 7));
}
Enter fullscreen mode Exit fullscreen mode

When using the above method this is what is shown with Intellisense.

shown with Intellisense

Using /doc feature of GitHub Copilot

public static class DateTimeExtensions
{ /// <summary> /// Calculates the first date of the week based on a given start day of the week. /// </summary> /// <param name="sender">The DateTime object representing the current date.</param> /// <param name="startOfWeek">The start day of the week (default is Sunday).</param> /// <returns>The first date of the week.</returns> public static DateOnly FirstDateOfWeek(this DateTime sender, DayOfWeek startOfWeek = DayOfWeek.Sunday) => DateOnly.FromDateTime(sender.AddDays(-1 * (7 + (sender.DayOfWeek - startOfWeek)) % 7));
}
Enter fullscreen mode Exit fullscreen mode

Much better with proper documentation from Copilot.

proper documentation from Copilot

Example 2

A developer comes across the following method has been asked to document the method.

public partial class Helpers
{ public static string NextValue(string sender, int incrementBy = 1) { string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0'); } [GeneratedRegex("[0-9]+$")] private static partial Regex NumbersPattern();
}
Enter fullscreen mode Exit fullscreen mode

To document the code.

  1. Select the method
  2. Press ALT + /
  3. Type / which provides options, select doc
  4. Press ENTER

Results

/// <summary>
/// Generates the next value based on the given sender string and incrementBy value.
/// </summary>
/// <param name="sender">The sender string.</param>
/// <param name="incrementBy">The value to increment by (default is 1).</param>
/// <returns>The next value.</returns>
public static string NextValue(string sender, int incrementBy = 1)
{ string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0');
}
Enter fullscreen mode Exit fullscreen mode

Next, jumping ahead to explaining code which will be covered later. A developer is curious how the above code works.

string value = NumbersPattern().Match(sender).Value; return sender[..^value.Length] + (long.Parse(value) + incrementBy) .ToString().PadLeft(value.Length, '0');
Enter fullscreen mode Exit fullscreen mode
  1. Select the following body of code.
  2. Press ALT + /
  3. Press /, select explain
  4. Press ENTER

The following is provided and at the bottom additional help.

Helpful information at bottom of explain dialog

The selected code is a part of the NextValue method in the Helpers class. Let’s break it down step by step:

  1. string value = NumbersPattern().Match(sender).Value; • This line retrieves the numeric value from the sender string using a regular expression pattern defined in the NumbersPattern method. • The NumbersPattern method is annotated with the [GeneratedRegex(“[0-9]+$”)] attribute, which indicates that it generates a regular expression pattern that matches one or more digits at the end of a string. • The Match method is called on the result of NumbersPattern() to find the first occurrence of the pattern in the sender string. • The Value property of the Match result is assigned to the value variable.
  2. return sender[..^value.Length] + (long.Parse(value) + incrementBy).ToString().PadLeft(value.Length, ‘0’);
  3. This line constructs and returns the next value based on the sender string and the incrementBy value.
  4. The sender[..^value.Length] expression is used to remove the numeric value from the end of the sender string. It uses the range operator .. to specify a range from the beginning of the string to the length of the sender string minus the length of the value string.
  5. (long.Parse(value) + incrementBy) parses the value string as a long integer and adds the incrementBy value to it.
  6. .ToString().PadLeft(value.Length, ‘0’) converts the sum to a string and pads it with leading zeros to match the length of the original value string.
  7. The resulting string is concatenated with the modified sender string and returned as the next value. In summary, the selected code extracts a numeric value from the sender string using a regular expression pattern, and then constructs the next value by incrementing the extracted value by the specified incrementBy value and padding it with leading zeros.

With both example code is document and in the last code has been broken down to understand code and in some cases assist if code needs modifications.

Using Copilot to explain code

Above an example for C# was presented for explaining code, Copilot can also explain JavaScript.

A developer wants to understand the following code is for a button click event but does not understand the code and would like to understand the code.

document.getElementById('getButton').addEventListener('click', function () { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://jsonplaceholder.typicode.com/users"); xhr.send(); xhr.responseType = "json"; xhr.onload = () => { if (xhr.readyState === 4 && xhr.status === 200) { const data = xhr.response; document.getElementById("people-template").innerHTML = ""; console.table(data, [ "id", "name", "email" ]); $('#people-template').append(` ${data.reduce((updated, user) => updated.concat(`<li>${user.id} <strong>Name</strong> ${user.name.padEnd(35, '.')} ${user.email}</li>`), '')}`); } else { console.log(`Error: ${xhr.status}`); } };
});
Enter fullscreen mode Exit fullscreen mode

Using /explain the following is provided.

The selected code is an event listener that triggers when the “Get” button is clicked. It performs an AJAX GET request to retrieve data from the “https://jsonplaceholder.typicode.com/users” URL. Let’s break down the code step by step:

  1. document.getElementById(‘getButton’).addEventListener(‘click’, function () { … });: This line adds an event listener to the “Get” button. When the button is clicked, the function inside the event listener will be executed.
  2. const xhr = new XMLHttpRequest();: This line creates a new XMLHttpRequest object. The XMLHttpRequest object is used to interact with servers and make HTTP requests.
  3. xhr.open(“GET”, “https://jsonplaceholder.typicode.com/users”);: This line initializes the request by specifying the HTTP method (GET) and the URL from which to retrieve data.
  4. xhr.send();: This line sends the HTTP request to the server.
  5. xhr.responseType = “json”;: This line sets the expected response type to JSON. This tells the browser to automatically parse the response as JSON.
  6. xhr.onload = () => { … };: This line sets up a callback function to handle the response when it is received. The function will be executed when the request is complete and the response is available.
  7. if (xhr.readyState === 4 && xhr.status === 200) { … }: This line checks if the request is complete (readyState 4) and if the response status is 200 (indicating a successful request).
  8. const data = xhr.response;: This line assigns the response data to the data variable.
  9. document.getElementById(“people-template”).innerHTML = “”;: This line clears the existing content of the “people-template” element.
  10. console.table(data, [“id”, “name”, “email”]);: This line logs the data in a table format to the console, displaying only the “id”, “name”, and “email” properties.
  11. $(‘#people-template’).append( … );: This line uses jQuery to append HTML content to the “people-template” element. It uses the data.reduce() method to iterate over the data array and generate a list item (
  12. ) for each user. The user’s ID, name, and email are displayed in the list item.
  13. console.log(Error: ${xhr.status});: If the request is not successful (status code other than 200), this line logs an error message to the console, including the status code. In summary, when the “Get” button is clicked, the code sends an AJAX GET request to retrieve user data from a remote server. If the request is successful, the data is displayed in a table format and appended to the “people-template” element as a list. If there is an error, an error message is logged to the console.

What about CSS?

A great many developers in the world do not have a grasp of working with CSS. With that in mind a developer wants to better understand the following rule.

legend { background: #0f506f; border: solid 1px black; -webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; padding: 6px; color: white;
}
Enter fullscreen mode Exit fullscreen mode

Use /explain and unlike the prior examples done in Visual Studio 2022, this one is done in VS Code.

Results from Copilot

Results from Copilot

What about SQL?

A developer comes across the following and wants to understand the statement.

WITH CTE
AS (SELECT t2.id, t2.TransDate AS [Date], t2.Credit, t2.Debit, SUM(COALESCE(t1.credit, 0) - COALESCE(t1.debit, 0)) AS Balance FROM dbo.Transactions t1 INNER JOIN dbo.Transactions t2 ON t1.TransDate <= t2.TransDate GROUP BY t2.TransDate, t2.Credit, t2.Debit, t2.id)
SELECT id, [Date], Credit, Debit
FROM CTE
ORDER BY CTE.[Date];
Enter fullscreen mode Exit fullscreen mode
  1. Select the statment
  2. Press ALT + /
  3. ENTER
  4. /explain
  5. Press ENTER

Copilot provides a detailed explanation along with a suggestion to modify the statement yet in this case is guessing and the guess is wrong but may open an idea up to a developer.

suggestion from above

What about documenting the statement.

  1. Select the statment
  2. Press ALT + /
  3. ENTER
  4. /doc
  5. Press ENTER
/* The following query uses a common table expression (CTE) to calculate the balance of transactions over time. It retrieves the transaction ID, date, credit amount, and debit amount from the Transactions table. The CTE calculates the balance by summing the difference between the credit and debit amounts for each transaction. The result is ordered by date in ascending order.
*/ WITH CTE
AS (SELECT t2.id, t2.TransDate AS [Date], t2.Credit, t2.Debit, SUM(COALESCE(t1.credit, 0) - COALESCE(t1.debit, 0)) AS Balance FROM dbo.Transactions t1 INNER JOIN dbo.Transactions t2 ON t1.TransDate <= t2.TransDate GROUP BY t2.TransDate, t2.Credit, t2.Debit, t2.id)
SELECT id, [Date], Credit, Debit
FROM CTE
ORDER BY CTE.[Date];
Enter fullscreen mode Exit fullscreen mode

Ask Copilot to refactor your code

To start off, general refactoring can be changing a method with a statement body to an expression body member using Visual Studio ReSharper extension.

Using statement body

public string[] Items()
{ return new[] { ApplicationName, ApplicationVersion, ApplicationKey };
}
Enter fullscreen mode Exit fullscreen mode

Refactor to expression body

public string[] Items() => new[] { ApplicationName, ApplicationVersion, ApplicationKey };
Enter fullscreen mode Exit fullscreen mode

Improve/refactor code with Copilot

Given the following method used for decades to return a list of primary keys from a database table a developer learns about Dapper and wants to rewrite this code using Dapper.

public List<int> CategoryIdentifiers()
{ var idList = new List<int>(); var selectStatement = "SELECT DISTINCT CategoryID FROM dbo.Categories "; using (var cn = new SqlConnection() { ConnectionString = ConnectionString }) { using (var cmd = new SqlCommand() { Connection = cn }) { cmd.CommandText = selectStatement; cn.Open(); var reader = cmd.ExecuteReader(); while (reader.Read()) { idList.Add(reader.GetInt32(0)); } } } return idList;
}
Enter fullscreen mode Exit fullscreen mode
  1. Select the above method
  2. Press ALT + /
  3. Enter Refactor this method to use Dapper.
  4. ENTER

The following is produced.

public List<int> CategoryIdentifiers()
{ var idList = new List<int>(); var selectStatement = "SELECT DISTINCT CategoryID FROM dbo.Categories"; using (var cn = new SqlConnection() { ConnectionString = ConnectionString }) { cn.Open(); idList = cn.Query<int>(selectStatement).AsList(); } return idList;
}
Enter fullscreen mode Exit fullscreen mode

Copilot did a great job on refactoring and works for inexperienced developers but an experienced developer can farther modify the code.

Here the experienced developer understands that Dapper will open the connection for us and the SQL statement does not really need to be formatted but imagine a large complex SQL statement with many joins, this would make a difference.

public List<int> CategoryIdentifiers()
{ using SqlConnection cn = new() { ConnectionString = ConnectionString }; return cn.Query<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """).AsList();
}
Enter fullscreen mode Exit fullscreen mode

Let’s now ask Copilot to make the above code asynchronous.

Copilot produces the following yet appears to change the SQL from a raw string literal to a verbatim string.

public async Task<List<int>> CategoryIdentifiersAsync()
{ await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>(@" SELECT DISTINCT CategoryID FROM dbo.Categories ")).AsList();
}
Enter fullscreen mode Exit fullscreen mode

ReSharper will provide a refactor recommendation and the developer accepts the recommendation.

public async Task<List<int>> CategoryIdentifiersAsync()
{ await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList();
}
Enter fullscreen mode Exit fullscreen mode

Ask Copilot to write a unit test

  1. Select the above method
  2. Press ALT + /
  3. Enter write a unit test
  4. ENTER

Note
We get a standard MS-Test but could specify which test framework to use.

[TestClass]
public class DapperOperationsTests
{ [TestMethod] public async Task CategoryIdentifiersAsync_ShouldReturnDistinctCategoryIDs() { // Arrange var dapperOperations = new DapperOperations(); // Act var result = await dapperOperations.CategoryIdentifiersAsync(); // Assert Assert.IsNotNull(result); CollectionAssert.AllItemsAreUnique(result); }
}
Enter fullscreen mode Exit fullscreen mode

Ask Copilot to add error handling

Add error handling with SeriLog logging to this method

  1. Select the above method
  2. Press ALT + /
  3. Enter add error handling with SeriLog logging to this method
  4. ENTER

The results were not what the developer wanted.

public async Task<List<int>> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>(@" SELECT DISTINCT CategoryID FROM dbo.Categories ")).AsList(); } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); throw; }
}
Enter fullscreen mode Exit fullscreen mode
  • We are back to a verbatim string, use ReSharper to use a raw string literal.
public async Task<List<int>> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList();
 } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); throw; }
}
Enter fullscreen mode Exit fullscreen mode

Let now go on without Copilot and get rid of throw and change the return type to a tuple which can be deconstructed by the caller.

public async Task<(bool, List<int>)> CategoryIdentifiersAsync()
{ try { await using SqlConnection cn = new() { ConnectionString = ConnectionString }; return (true,(await cn.QueryAsync<int>("""
 SELECT DISTINCT CategoryID FROM dbo.Categories """)).AsList());
 } catch (Exception ex) { Log.Error(ex, "An error occurred while executing CategoryIdentifiersAsync"); return (false, null); }
}
Enter fullscreen mode Exit fullscreen mode

Unit test revision

Now that there has been a change to the return type ask Copilot to write a new unit test.

[TestClass]
public class DapperOperationsTests
{ [TestMethod] public async Task CategoryIdentifiersAsync_ShouldReturnCategoryIDs() { // Arrange DapperOperations dapperOperations = new DapperOperations(); // Act (bool success, List<int> categoryIds) = await dapperOperations.CategoryIdentifiersAsync(); // Assert Assert.IsTrue(success); Assert.IsNotNull(categoryIds); Assert.IsTrue(categoryIds.Count > 0); }
}
Enter fullscreen mode Exit fullscreen mode

Unit test

In each example for unit test only one test was written, a developer should do a mirror image of a good test and have a one or more test for when things break.

Asking Copilot to write new code

Copilot can write code, for instance a developer needs a string language extension method that splits a string as follows, FirstName to First Name. The developer finds the following language extension but thinks there may be a more efficient way and not using regular expressions.

public static class StringExtensions
{ private static readonly Regex CamelCaseRegex = new(@"([A-Z][a-z]+)"); /// <summary> /// KarenPayne => Karen Payne /// </summary> [DebuggerStepThrough] public static string SplitCamelCase(this string sender) => string.Join(" ", CamelCaseRegex.Matches(sender) .Select(m => m.Value));
}
Enter fullscreen mode Exit fullscreen mode

They ask several questions, but none are sufficient but than decide to include using span and the following is provided.

public static class StringExtensions
{ /// <summary> /// Split text at each capital letter /// </summary> /// <param name="input">string to work on</param> /// <returns> /// <para>An empty string, if the input is null or empty.</para> /// <para>Same as original if nothing affected</para> /// <para>String split on each uppercase token</para> /// <para>SSMS would become S S M S</para> /// </returns> [DebuggerStepThrough] public static string SplitCamelCase(this string input) { if (string.IsNullOrEmpty(input)) { return input; } Span<char> result = stackalloc char[input.Length * 2]; var resultIndex = 0; for (var index = 0; index < input.Length; index++) { var currentChar = input[index]; if (index > 0 && char.IsUpper(currentChar)) { result[resultIndex++] = ' '; } result[resultIndex++] = currentChar; } return result[..resultIndex].ToString(); }
}
Enter fullscreen mode Exit fullscreen mode

Note
Of course, some developers favor shorter code as in the first code written yet shorter code is not always the best so if curious run both through Benchmarks.

Interface and classes

The following is a very simple example for using AI to create interfaces and classes in C# using GitHub Copilot and JetBrains ReSharper.

Note
Jetbrains ReSharper is a paid extension for Visual Studio. It is used here to move the generated code to proper folders and update the namespaces. Also, Jetbrains has a paid AI service which Karen has a ticket for.

Here we want to create

  • A generic interface
  • Customer class to implement above interface
  • Both of the above get created in Program.cs using Copilot.

✔️ The interface are then moved using Jetbrain’s ReSharper Visual Studio extension to proper folders.

✔️ During the ReSharper action Visual Studio kicks in and asked to update the namespace.

A console project is used for this experiment in Program.cs.

Question 1
Create a public crud interface named IBase under Program.cs

Copilot responds with the following.

namespace InterfacesClassesExamples
{ public interface IBase<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(T entity); } internal partial class Program { static void Main(string[] args) { } }
}
Enter fullscreen mode Exit fullscreen mode

Question 2
Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property.

Copilot responds with the following.

namespace InterfacesClassesExamples
{ public interface IBase<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); void Add(T entity); void Update(T entity); void Delete(T entity); } public class Customer : IBase<Customer>, INotifyPropertyChanged { private int _id; private string _firstName; private string _lastName; private string _email; private DateOnly _birthDate; public int Id { get { return _id; } set { if (_id != value) { _id = value; OnPropertyChanged(nameof(Id)); } } } public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } public Customer GetById(int id) { // Implementation of GetById method throw new NotImplementedException(); } public IEnumerable<Customer> GetAll() { // Implementation of GetAll method throw new NotImplementedException(); } public void Add(Customer entity) { // Implementation of Add method throw new NotImplementedException(); } public void Update(Customer entity) { // Implementation of Update method throw new NotImplementedException(); } public void Delete(Customer entity) { // Implementation of Delete method throw new NotImplementedException(); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } internal partial class Program { static void Main(string[] args) { } }
}
Enter fullscreen mode Exit fullscreen mode

Next steps, create an Interface and Models folder and move IBase to the Interface folder and Customer to the Model folder or copy IBase and Customer to appropriate folders in another project. ReSharper can assist with moving IBase and Customer to their own files and Visual Studio will assist with setting up proper namespacing.

Now suppose the above was done in the same console project but in a class outside of Program.cs the results vary.

In one case the following

public class Customer : IBase<Customer>, INotifyPropertyChanged
Enter fullscreen mode Exit fullscreen mode

Is created as

public class Customer : IBase<int>, INotifyPropertyChanged
Enter fullscreen mode Exit fullscreen mode

In another test in a separate class the following question was posed to Copilot.

Create a public crud generic interface named IBase with a constraint on T. create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property

The response was missing private fields for property change notification and IBase was not implemented and duplicate property names.

namespace InterfacesClassesExamples.Test
{ public interface IBase<T> where T : class { void Create(T entity); T Read(int id); void Update(T entity); void Delete(int id); } public class Customer : IBase<Customer>, INotifyPropertyChanged { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateOnly BirthDate { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // Implement INotifyPropertyChanged for each property public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } // Implement INotifyPropertyChanged for each property public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } // Implement INotifyPropertyChanged for each property public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } // Implement INotifyPropertyChanged for each property public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } }
}
Enter fullscreen mode Exit fullscreen mode

Next, still in the same class as above.

  1. Create a public crud interface named IBase
  2. Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property.

Results with no change property notification code.

namespace InterfacesClassesExamples.Test
{ public interface IBase<T> where T : class { T GetById(int id); void Add(T entity); void Update(T entity); void Delete(T entity); } public class Customer : IBase<Customer>, INotifyPropertyChanged { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateOnly BirthDate { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public Customer GetById(int id) { // Implementation of GetById method } public void Add(Customer entity) { // Implementation of Add method } public void Update(Customer entity) { // Implementation of Update method } public void Delete(Customer entity) { // Implementation of Delete method } }
}
Enter fullscreen mode Exit fullscreen mode

Next, here the experiment was done in a new class project.

Ask Copilot the same questions as in prior failures and failed again.

Instead of

public class Customer : IBase<Customer>, INotifyPropertyChanged
Enter fullscreen mode Exit fullscreen mode

Copilot produced.

public class Customer : IBase, INotifyPropertyChanged
Enter fullscreen mode Exit fullscreen mode

Then when ready to try again the following was suggested without property change notification.

copilot suggestion without asking

Wait a minute

Let’s try the last question as follows.

_Create a public class named Customer which implements IBase with the following properties, Id, FirstName, LastName, Email and BirthDate as DateOnly and implement INotifyPropertyChanged for each property
_

And we get. Next step, move Customer to it’s own file.

using System.ComponentModel; namespace InterfacesClassesExamplesLibrary.Interfaces
{ public class Customer : IBase<Customer>, INotifyPropertyChanged { private int _id; private string _firstName; private string _lastName; private string _email; private DateOnly _birthDate; public int Id { get { return _id; } set { if (_id != value) { _id = value; OnPropertyChanged(nameof(Id)); } } } public string FirstName { get { return _firstName; } set { if (_firstName != value) { _firstName = value; OnPropertyChanged(nameof(FirstName)); } } } public string LastName { get { return _lastName; } set { if (_lastName != value) { _lastName = value; OnPropertyChanged(nameof(LastName)); } } } public string Email { get { return _email; } set { if (_email != value) { _email = value; OnPropertyChanged(nameof(Email)); } } } public DateOnly BirthDate { get { return _birthDate; } set { if (_birthDate != value) { _birthDate = value; OnPropertyChanged(nameof(BirthDate)); } } } public Customer GetById(int id) { // Implementation of GetById method } public void Insert(Customer entity) { // Implementation of Insert method } public void Update(Customer entity) { // Implementation of Update method } public void Delete(Customer entity) { // Implementation of Delete method } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public interface IBase<T> { T GetById(int id); void Insert(T entity); void Update(T entity); void Delete(T entity); }
}
Enter fullscreen mode Exit fullscreen mode

Lessons learn for generating new code

  • Location of where the question is asked determines how copilot will respond and how the question is asked will also determine copilot’s response.
  • Sometimes it is faster to just write the code by hand if responding solutions are not to a developers requirements.

Tip

First out questions in a text file, ask Copilot, if the response is not proper, go back and create a copy of the question in the text file and rephrase. Try until a good enough response is provided or go old school and write code yourself.

Debugging

When GitHub Copilot is active and a runtime exception is raised a developer can click, Ask Copilot to get suggestions which is what the project is for. Once asking coilot a window opens with recommendations and in most cases why the exception was thrown.

The connection string is deliberately setup to point to a non-existing SQL-Server instance and the default time out has been altered from the original which is 30 seconds down to two seconds.

Ask copilot while debugging

Note
This project was created to show a simple example for SQL-Server computed columns and was put here to show the above.

About (When the project has no issues)

Provides an interesting way to compute how old a person is in years.

Original code came from this Stackoverflow post.

I took the code and created a simple Console project to demonstrate how to use it with a computed column in a SQL-Server database table using Dapper to read the data.

CREATE TABLE [dbo].[BirthDays]( [Id] [int] IDENTITY(1,1) NOT NULL, [FirstName] [nvarchar](max) NULL, [LastName] [nvarchar](max) NULL, [BirthDate] [date] NULL, [YearsOld] AS ((CONVERT([int],format(getdate(),'yyyyMMdd'))-CONVERT([int],format([BirthDate],'yyyyMMdd')))/(10000))
Enter fullscreen mode Exit fullscreen mode
  • Take birthdate and current date, format and convert to integer
  • Subtract birthdate from current date
  • Divide by 10,000 to get years old

Source code

Do not expect the code provided to be stable as it was used for demonstrating what has been provided above.

Source code

VS Code

Using GitHub Copilot works great here as well as Visual Studio.

The shortcut is CTRL + I rather than ALT + / in Visual Studio.

A great usage is fixing code, in the screenshot below there are errors on each input because they are not associated with labels. Hover and a menu appears, use the last menu item to see why this is an issue (a WCAG AA rule) and second from bottom to fix.

VS Code showing part of an html file

Let’s fix the issue with copilot by associating labels above inputs with the proper input and for kicks, surround the labels and inputs with a fieldset.

Press CTRL + I and ask with

wrap in fieldset and legend then associate inputs with labels

before and after asking copilot

Copilot History

In VS Code, use the arrow keys to traverse history for the current session while Visual Studio does not have history available.

Security

Use caution to what text is placed into your Copilot questions as they very well be seen by other users of Copilot. In one case while writing documentation into a markdown file Copilot suggested text that was from a closed system. They were informed that this happened. Imagine exposing customer information or internal server details and a bad actor got the information what might happen?

Summary

Made it this far? That means by applying what was written the reader will have a better expectation of Copilot. Copilot is a great edition to programmers at every level but one should not rely solely on Copilot as seen not all response will be correct. As Copilot matures expect better responses and more features.


Discover more from Coursity

Subscribe to get the latest posts to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *