Visual Studio Extension – Part 1

Visual Studio Extension – Part 1

One of the little side projects I finished recently was an “add new item” extension, for Visual Studio. This particular extension generates a new repository class, for a C# project.

Personally, I use repositories often but I’m not always in a position to leverage one of my libraries, so I can’t always derive from a repository base class I know, and trust. In those cases, I’m forced to either use someone else’s base class, or, write everything from scratch.

Luckily, writing everything from scratch is no longer a problem, since I can now use my handy dandy repository extension, in Visual Studio, to create a new repository whenever the need arises.

The project’s source code is available HERE

The compiled (but unsigned) VSIX project is available HERE

Before I cover the extension, let me say that I created a test project and included it on GITHUB, called TestDAL. That project is perfect for testing this VS extension, because it has a model, an EFCORE data-context, and a folder for creating new repository classes. I’ll be using the TestDAL project to demonstrate my VS extension. I’ll assume anyone following along is using the same project.

Installing the VSIX, or running the project’s source code, results in a new VS item template extension called ‘CodeGator Repository’. The easiest way to demonstrate the extension is to highlight a folder, in my case I’ll highlight the Repository folder, in my TestDAL project. Once I’ve done that, I usually right click and choose the “Add | New Item …” menu choices.

That brings up the “Add New Item” dialog, in Visual Studio. From there, go find the “CodeGator Repository” item and select it, then press the “Add” button.

That brings up the welcome page of my template wizard UI.

Press the next button and it’s going to ask you to decide on a repository type.

The choices are: Default, and EfCore. A default repository is just an empty shell. You get to fill in all the code to make it actually do anything. This is perfect for any repository that isn’t EFCORE based. For an EFCORE repository, choose EfCore from the dropdown.

Since we’re all using the TestDAL project I included with the source code (you ARE using the TestDAL project, right?) then we can quickly get to the test data-context I provided for this purpose. Just select the TestDAL project from the first dropdown, then the namespace from the second dropdown, and finally the TestDataContext from the name dropdown.

This step also includes a checkbox that says “Use Data-Context Factory”. What does that do? Well, if your repository is going to be created through a Dependency Injection (DI) container, and it’s going to be registered with scope lifetime, then you’ll want to uncheck that checkbox so you’ll get a repository that contains an instance of your selected data-context, in the repository’s constructor.

On the other hand, if you’re not using DI, or your repository is not registered with scope lifetime, then it’s possible you’ll want to protect yourself from having multiple threads hit your data-context, through your repository, at the same time. EFCORE frowns upon the whole multiple threads hitting a data-context simultaneously thing. So, in that case, you’ll want to check the checkbox and then the extension will give you a repository that contains a data-context factory, for creating data-context instances at runtime. That way, you can hit your repository with as many threads as you like and EFCORE won’t hate you for it.

Either way, press the next button to continue.

The next thing we have to do is decide what class we want to use as a model type, for our repository. Now, of course, we’re making a big assumption that there’s only one model class for our repository. I realize that’s not always true. But, it’s true most of the time so I went with that assumption. Good news is, we’re generating C# code here so you can always change things, later, if you feel the need.

After selecting a model type, press the next button.

Now, I realize we got the chance to name our repository class when we started all of this, but, I wanted the ability to make some decisions about naming, here, in case we’ve changed our minds by this time. In any event, choose some legal C# values for the namespace, interface name, and class name. (The wizard will default this name based on whatever model you selected)

Press the next button to comtinue.

This steps allows us to generate frequently used repository methods. These are the CRUD methods most repository classes need. You can uncheck anything you don’t want.

Press the next button to continue.

This last screen gives us the ability to look and what we’ve chosen, in each step, and possibly go back and make changes.

Assuming you’re alright with everything you chose, press the Finish button to create the actual repository.

Assuming you followed along, and chose the same things I did, this is what your repository interface is going to look like:

#region Local using statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using TestDAL.Models;

#endregion

namespace TestDAL.Repositories
{
    /// <summary>
    /// This interface represents an object that manages the storage and retrieval
    /// of <see cref="Foo"/> objects.
    /// </summary>
    public interface IFooRepository
    {

        /// <summary>
        /// This method creates a new <see cref="Foo"/> object in the 
        /// underlying storage.
        /// </summary>
        /// <param name="model">The model to create in the underlying storage.</param>
        /// <param name="cancellationToken">A cancellation token that is monitored
        /// for the lifetime of the method.</param>
        /// <returns>A task to perform the operation that returns the newly created
        /// <see cref="Foo"/> object.</returns>
        /// <exception cref="ArgumentException">This exception is thrown whenever one
        /// or more arguments are missing, or invalid.</exception>
        /// <exception cref="RepositoryException">This exception is thrown whenever the
        /// repository fails to complete the operation.
        Task<Foo> CreateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            );

        /// <summary>
        /// This method deletes an existing <see cref="Foo"/> object from the 
        /// underlying storage.
        /// </summary>
        /// <param name="model">The model to delete from the underlying storage.</param>
        /// <param name="cancellationToken">A cancellation token that is monitored
        /// for the lifetime of the method.</param>
        /// <returns>A task to perform the operation.</returns>
        /// <exception cref="ArgumentException">This exception is thrown whenever one
        /// or more arguments are missing, or invalid.</exception>
        /// <exception cref="RepositoryException">This exception is thrown whenever the
        /// repository fails to complete the operation.
        Task DeleteAsync(
            Foo model,
            CancellationToken cancellationToken = default
            );

        /// <summary>
        /// This method searches for matching <see cref="Foo"/> objects using
        /// the given LINQ expression.
        /// </summary>
        /// <param name="expression">The LINQ expression to use for the search.</param>
        /// <param name="cancellationToken">A cancellation token that is monitored
        /// for the lifetime of the method.</param>
        /// <returns>A task to perform the operation that returns the results of the search.</returns>
        /// <exception cref="ArgumentException">This exception is thrown whenever one
        /// or more arguments are missing, or invalid.</exception>
        /// <exception cref="RepositoryException">This exception is thrown whenever the
        /// repository fails to complete the operation.
        Task<IEnumerable<Foo>> FindAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            );

        /// <summary>
        /// This method searches for a single matching <see cref="Foo"/> object using
        /// the given LINQ expression.
        /// </summary>
        /// <param name="expression">The LINQ expression to use for the search.</param>
        /// <param name="cancellationToken">A cancellation token that is monitored
        /// for the lifetime of the method.</param>
        /// <returns>A task to perform the operation that returns a matching <see cref="Foo"/> 
        /// object, if one was found, or NULL otherwise.</returns>
        /// <exception cref="ArgumentException">This exception is thrown whenever one
        /// or more arguments are missing, or invalid.</exception>
        /// <exception cref="RepositoryException">This exception is thrown whenever the
        /// repository fails to complete the operation.
        Task<Foo?> FindSingleAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            );

        /// <summary>
        /// This method updates an existing <see cref="Foo"/> object in the 
        /// underlying storage.
        /// </summary>
        /// <param name="model">The model to update in the underlying storage.</param>
        /// <param name="cancellationToken">A cancellation token that is monitored
        /// for the lifetime of the method.</param>
        /// <returns>A task to perform the operation that returns the newly updated
        /// <see cref="Foo"/> object.</returns>
        /// <exception cref="ArgumentException">This exception is thrown whenever one
        /// or more arguments are missing, or invalid.</exception>
        /// <exception cref="RepositoryException">This exception is thrown whenever the
        /// repository fails to complete the operation.
        Task<Foo> UpdateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            );

    }
}

The XML comments are there so, when I come back to this code a few years from now, I won’t have to remember what each method was supposed to do. Also, because its XML comments, we can generate pretty online technical help for our project, later.

Here is what the repository class itself looks like:

#region Local using statements
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using TestDAL.EfCore;
using TestDAL.Models;

#endregion

namespace TestDAL.Repositories
{
    /// <summary>
    /// This class is an EFCORE implementation of the <see cref="IFooRepository"/>
    /// interface.
    /// </summary>
    internal class FooRepository : IFooRepository
    {
        // *******************************************************************
        // Fields.
        // *******************************************************************

        #region Fields

        /// <summary>
        /// This field contains the EFCORE data-context factory for this repository.
        /// </summary>
        protected readonly IDbContextFactory<TestDataContext> _dbContextFactory;

        /// <summary>
        /// This field contains the logger for this repository.
        /// </summary>
        protected readonly ILogger<FooRepository> _logger;

        #endregion

        // *******************************************************************
        // Constructors.
        // *******************************************************************

        #region Constructors

        /// <summary>
        /// This constructor creates a new instance of the <see cref="FooRepository"/>
        /// class.
        /// </summary>
        /// <param name="dbContextFactory">The EFCORE data-context factory
        /// to use with this repository.</param>
        /// <param name="logger">The logger to use with this repository.</param>
        public FooRepository(
            IDbContextFactory<TestDataContext> dbContextFactory,
            ILogger<FooRepository> logger
            )
        {
            // Save the reference(s).
            _dbContextFactory = dbContextFactory;
            _logger = logger;
        }

        #endregion

        // *******************************************************************
        // Public methods.
        // *******************************************************************

        #region Public methods

        /// <inheritdoc/>
        public virtual async Task<bool> AnyAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a new Foo instance in the " +
                    "TestDataContext data-context"
                    );

                // Create the entity in the data-store.
                var data = await dbContext.Set<Foo>().AnyAsync(
                    expression,
                    cancellationToken
                    ).ConfigureAwait(false);

                // Return the results.
                return data;
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to search for matching Foo instances in " +
                    "the TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to search for matching " +
                    "Foo instances in the TestDataContext data-context!",
                    innerException: ex
                    );
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo> CreateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a new Foo instance in the " +
                    "TestDataContext data-context"
                    );

                // Create the entity in the data-store.
                var entity = await dbContext.Set<Foo>()
                    .AddAsync(
                        model,
                        cancellationToken
                        ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Saving changes to the TestDataContext data-context"
                    );

                // Save the changes.
                await dbContext.SaveChangesAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Return the results.
                return entity.Entity;
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to create a Foo instance in the " +
                    "TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to create a Foo " +
                    "instance in the TestDataContext data-context!",
                    innerException: ex
                    );
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task DeleteAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Attaching a Foo instance to the TestDataContext " +
                    "data-context"
                    );

                // Get a tracked instance from the data-store.
                var trackedMimeType = dbContext.Set<Foo>().Attach(
                    model
                    );

                // Log what we are about to do.
                _logger.LogDebug(
                    "Removing a Foo instance from the " +
                    "TestDataContext data-context"
                    );

                // Remove the entity from the data-store.
                var entity = dbContext.Set<Foo>()
                    .Remove(
                        trackedMimeType.Entity
                        );

                // Log what we are about to do.
                _logger.LogDebug(
                    "Saving changes to the TestDataContext data-context"
                    );

                // Save the changes.
                await dbContext.SaveChangesAsync(
                    cancellationToken
                    ).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to remove a Foo instance from the " +
                    "TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to remove a Foo " +
                    "instance from the TestDataContext data-context!",
                    innerException: ex
                    );
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<IEnumerable<Foo>> FindAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Searching for matching Foo instances from" +
                    "a TestDataContext data-context"
                    );

                // Perform the search.
                var data = await dbContext.Set<Foo>().Where(expression)
                    .ToListAsync(
                        cancellationToken
                        ).ConfigureAwait(false);

                // Return the result.
                return data;
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to search for matching Foo instances from " +
                    "the TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to search for matching " +
                    $"Foo instances from the TestDataContext " +
                    "data-context!",
                    innerException: ex
                    );
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo?> FindSingleAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Searching for a matching Foo instance from" +
                    "a TestDataContext data-context"
                    );

                // Perform the search.
                var data = await dbContext.Set<Foo>().Where(
                    expression
                    ).FirstOrDefaultAsync(
                        cancellationToken
                        ).ConfigureAwait(false);

                // Return the result.
                return data;
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to search for a matching Foo instance from " +
                    "the TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to search for a matching " +
                    $"Foo instance from the TestDataContext " +
                    "data-context!",
                    innerException: ex
                    );
            }
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo> UpdateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            try
            {
                // Log what we are about to do.
                _logger.LogDebug(
                    "Creating a TestDataContext data-context"
                    );

                // Create a database context.
                using var dbContext = await _dbContextFactory.CreateDbContextAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Log what we are about to do.
                _logger.LogDebug(
                    "Attaching a Foo instance to the TestDataContext " +
                    "data-context"
                    );

                // Get a tracked instance from the data-store.
                var trackedMimeType = dbContext.Set<Foo>().Attach(
                    model
                    );

                // Log what we are about to do.
                _logger.LogDebug(
                    "Updating a Foo instance from the " +
                    "TestDataContext data-context"
                    );

                // Update the entity.
                dbContext.Set<Foo>().Update(
                    trackedMimeType.Entity
                    );

                // Log what we are about to do.
                _logger.LogDebug(
                    "Saving changes to the TestDataContext data-context"
                    );

                // Save the changes.
                await dbContext.SaveChangesAsync(
                    cancellationToken
                    ).ConfigureAwait(false);

                // Return the results
                return trackedMimeType.Entity;
            }
            catch (Exception ex)
            {
                // Log what happened.
                _logger.LogError(
                    ex,
                    "Failed to update a Foo instance in the " +
                    "TestDataContext data-context"
                    );

                // Provider better context.
                throw new RepositoryException(
                    message: $"The repository failed to update a Foo " +
                    "instance in the TestDataContext data-context!",
                    innerException: ex
                    );
            }
        }

        #endregion
    }
}

The code is pretty simple because, well, these are simple CRUD methods. But, since the code is generated, and not derived from, you can go crazy making whatever changes you like without hurting my feelings. No, seriously, I’m alright with that … Change whatever you like … *sniff*

There is also a RepositoryException class that gets generated. I won’t cover that code because it’s just an exception class.

So the nice thing is, with this tool, you create repository classes whenever and wherever you need them. If you’re not into EFCORE that’s fine. Here is what the “default” repository class looks like, when it is generated:

#region Local using statements
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using TestDAL.Models;

#endregion

namespace TestDAL.Repositories
{
    /// <summary>
    /// This class is a default implementation of the <see cref="IFooRepository"/>
    /// interface.
    /// </summary>
    internal class FooRepository : IFooRepository
    {
        // *******************************************************************
        // Public methods.
        // *******************************************************************

        #region Public methods

        /// <inheritdoc/>
        public virtual async Task<bool> AnyAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo> CreateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task DeleteAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<IEnumerable<Foo>> FindAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo?> FindSingleAsync(
            Expression<Func<Foo, bool>> expression,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        // *******************************************************************

        /// <inheritdoc/>
        public virtual async Task<Foo> UpdateAsync(
            Foo model,
            CancellationToken cancellationToken = default
            )
        {
            // TODO : write the code for this.
            throw new NotImplementedException();
        }

        #endregion
    }
}

In this case, it’s a simple code outline for a repository.

Repository classes are easy to write but after a few years they also become a bit tedious to crank out, over and over. This tool gives any developer a quick way to generate a repository without doing it the old fashioned way.

Next time I’ll cover the internals of the template wizard project. Check back then.

Photo by Dmitry Vechorko on Unsplash