Saturday 9 July 2022

MediatR - Adding a pipeline behavior and logging to SeriLog and Seq

This article will present how you can add a pipeline behavior to MediatR and then log to SeriLog and Seq. This makes it possible to add for example logging and use the 'decorator pattern' with MediatR to add behavior to the pipeline of Asp.net core in general (.net 6 is used here). It is therefore sort of Middelware and tied together via MediatR. First off, lets define the IPipelineBehavior, which will do our logging. Note that you have to add the generic type parameter where condition here of TRequest. Note that we inject the ILogger<LoggingBehavior<TRequest, TResponse>> logger object here. In .net (core) we send in ILogger instance of the type of the class we are injecting into to log out to log sources and SeriLog will present this information. Inside Seq we can also browse this log information. We only log payloads if we are inside Development environment and the payload of request / response can be serialized and is below 50 kB.
 
 Program.cs
 
using MediatR;
using Newtonsoft.Json;
using System.Diagnostics;

namespace CqrsDemoWebApi.Features.Books.Pipeline
{

    public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
        private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
        private readonly IWebHostEnvironment _environment;

        public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger, IWebHostEnvironment environment)
        {
            _logger = logger;
            _environment = environment;
        }

        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            string? requestData = null;

            if (_environment.IsDevelopment())
            {
                //log serializable payload data in request (as json format) to SeriLog if we are inside Development environment and the payload is below 50 kB in serializable size
                try
                {
                    requestData = request != null ? JsonConvert.SerializeObject(request) : null;
                    if (requestData?.Length > 50 * 1028)
                        requestData = null; //avoid showing data larger than 100 kB in serialized form in the logs of Seq / SeriLog                    
                }
                catch (Exception)
                {
                    //ignore 
                }
            }
            _logger.LogInformation($"Handling request {typeof(TRequest).Name} {{@requestData}}", requestData);
        
            var response = await next();

            string? responseData = null;
            if (_environment.IsDevelopment())
            {              
                //log serializable payload data in response (as json format) to SeriLog if we are in Development environment and the payload is below 50 kB in serialized size
                try
                {
                    responseData = response != null ? JsonConvert.SerializeObject(response) : null;
                    if (responseData?.Length > 50 * 1028)
                        responseData = null; //avoid showing data larger than 100 kB in serialized form in the logs of Seq / SeriLog
                }
                catch (Exception)
                {
                    //ignore 
                }
            }
            _logger.LogInformation($"Handled request {typeof(TRequest).Name}. Returning result of type {typeof(TResponse).Name} {{@responseData}}.", responseData);
            return response;
        }

    }

}
 
 
Afterwards register the IPipelineBehavior. Note how we also add UseSerilog method here and add Seq too.
 
 Program.cs
 
using CqrsDemoWebApi.Database;
using CqrsDemoWebApi.Features.Books.Pipeline;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, lc) =>
 lc.WriteTo.Console()
 .WriteTo.Seq("http://localhost:5341")); //note - to use Seq with this url - install it from here (free individual license) : https://datalust.co/download

// Add services to the container.

builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); //adding MediatR support here. 
//register some pipeline(s) defined for MediatR usage
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); 

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = new ConnectionString(builder.Configuration.GetConnectionString("CqrsDemoBooksDb")); 
builder.Services.AddSingleton(connectionString);

builder.Services.AddDbContext<BooksDbContext>(options =>
options.UseSqlServer(connectionString.Value)); 

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
 
 
Here are the updated nuget packages inside the csproj file of the Asp.net core project (.net 6) :
 
 
  <ItemGroup>
    <PackageReference Include="Mediatr" Version="10.0.1" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
    <PackageReference Include="Serilog.AspNetCore" Version="6.0.0-dev-00265" />
    <PackageReference Include="Serilog.Sinks.Seq" Version="5.1.2-dev-00225" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>
 
 
We also need to install Seq on our computer to see the messages sent to SeriLog inside Seq. Go to this url to download free individual license of Seq : https://datalust.co/download You can see the logs at Seq's default url and also set up in Program.cs file : http://localhost:5341

Friday 8 July 2022

Using MediatR to implement CQRS pattern with EF Core

This article will present a sample solution I have pushed to GitHub. It uses MediatR library to showcase CQRS pattern together with .NET 6 and Entity Framework Core. MediatR takes care of decoupling the sending of messages to handling messages in-process. It can be used in the data layer together with EF Core for example to dispatch (send) messages which will either handle (execute) a 'command' (defined in CQRS as a 'write operation') or process and return a 'query' (defined in CQRS as a 'read' operation). What we gain from CQRS is the ability to offload the sending of these messages (commands and queries) to the correct handler for given message handler (executing either a 'command' or return the results of a 'query') and this makes it easier to create scalable code in the data layer. It gives thinner Asp.net core (web api) controller actions in the sample demo. First off, clone the solution :
git clone https://github.com/toreaurstadboss/CqrsDemoWebApi.git
The following Nuget packages are relevant for EF Core and MediatR.
 
 
    <PackageReference Include="Mediatr" Version="10.0.1" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1"> 
 
 
 
The Wiki of MediatR contains some example code here: https://github.com/jbogard/MediatR/wiki We will first look into the Request /Reponse messages, dispatched to a single handler. These are either comands or queries in this sample. Let us first consider a query that returns a list of books. Here is how it is implemented :
 
 
using CqrsDemoWebApi.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace CqrsDemoWebApi.Features.Books.Query
{
    public class GetBooks
    {

        public class Query : IRequest<IEnumerable<Book>>
        {

        }

        public class QueryHandler : IRequestHandler<Query, IEnumerable<Book>>
        {
            private readonly BooksDbContext _db;

            public QueryHandler(BooksDbContext db)
            {
                _db = db;
            }

            public async Task<IEnumerable<Book>> Handle(Query request, CancellationToken cancellationToken)
            {
                return await _db.Books.ToListAsync(cancellationToken); 
            }
        }
    }
}
 
 
We see that we are supposed to handle the query async since we must return a Task of TResponse (although we could use Task.FromResult here if we wanted to return it synchronously..) where the TRequest is of type IRequest. Note that Query only implements IRequest of the return type and has no properties (i.e. 'request parameters') since we return all books here and need no such parameters. We inject the DbContext here via dependency injection. In Program.cs of the .NET 6 solution we add MediatR using the builder.Services.AddMediatR() method:
 
  Program.cs (.NET 6 Web Api project)

using CqrsDemoWebApi.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); //adding MediatR support here. 
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = new ConnectionString(builder.Configuration.GetConnectionString("CqrsDemoBooksDb")); 
builder.Services.AddSingleton(connectionString);

builder.Services.AddDbContext<BooksDbContext>(options =>
options.UseSqlServer(connectionString.Value)); 

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
 
Nothing special here, just basic setup of a standard web api in Net 6 here, using the AddMediatR method pointing to the assembly where our contracts reside (commands and queries and their respective handlers). The BookController web api controller looks like this :
 

using CqrsDemoWebApi.Database;
using CqrsDemoWebApi.Features.Books;
using CqrsDemoWebApi.Features.Books.Command;
using CqrsDemoWebApi.Features.Books.Query;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace CqrsDemoWebApi.Controllers
{

    [Route("api/[controller]")]
    public class BookController : ControllerBase
    {
        private readonly IMediator _mediator;

        public BookController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        public async Task<IEnumerable<Book>> GetBooks() 
        {
            var books = await _mediator.Send(new GetBooks.Query());
            return books; 
        }

        [HttpGet("{id}")]
        public async Task<Book> GetBook(int id)
        {
            var book = await _mediator.Send(new GetBooksById.Query { Id = id });
            return book; 
        }

        [HttpPost]
        public async Task<ActionResult> AddBook([FromBody] AddNewBook.Command command)
        {
            var createdBookId = await _mediator.Send(command);
            return CreatedAtAction(nameof(GetBook), new { id = createdBookId }, null); 
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteBook(int id)
        {
            await _mediator.Send(new DeleteCommand.Command { Id = id });
            return NoContent(); 
        }

    }
}

 
 
As we see, we inject the IMediator here and then use the Dependency Injected field to send the command of query instance. Note the use of [FromBody] in Post requests to send the POST body into the handler for the Command. Each query or command is implemented as nested classes where Query and QueryHandler is nested inside, implement IRequest of T and IRequestHandler of T (and return type U back again if we want a return result). If we do not
want a return result 'void' - we use the special type Unit. This signals that we do not return any results from the command or the query. We can return an int value for the Command after we have inserted a row. It is okay in CQRS to return a resource identifier. CQRS allows also return results from command as long as it is a resource identifier to use to look up the data via a query. (it could be Guid etc too if that is suitable). Here is a example of a command that creates a book:
 
 
 using CqrsDemoWebApi.Database;
using MediatR;

namespace CqrsDemoWebApi.Features.Books.Command
{
    public class AddNewBook
    {
        public class Command : IRequest<int>
        {
            public string? Title { get; set; }
            public string? Author { get; set; }
            public int Year { get; set; }
            public string? ImageLink { get; set; }
            public int Pages { get; set; }
            public string? Country { get; set; }
            public string? Language { get; set; }
            public string? Link { get; set; }
        }

        public class CommandHandler : IRequestHandler<Command, int>
        {
            private readonly BooksDbContext _booksDbContext;

            public CommandHandler(BooksDbContext booksDbContext)
            {
                _booksDbContext = booksDbContext;
            }

            public async Task<int> Handle(Command request, CancellationToken cancellationToken)
            {
                var book = new Book
                {
                    Title = request.Title,
                    Author = request.Author,    
                    Year = request.Year,
                    ImageLink = request.ImageLink,
                    Pages = request.Pages,
                    Country = request.Country,
                    Language = request.Language,
                    Link = request.Link
                };

                await _booksDbContext.Books.AddAsync(book, cancellationToken);
                _booksDbContext.SaveChanges();
                return book.BookId;                          
            }
        }

    }
}
 
 
A sample json body for the POST is then:
 
 
 {
   "title": "Mythical Man Month",
   "author": "Frederick Brooks",
   "year": 1975,
   "country": "USA",
   "imagelink": "",
   "pages": 222
  }
 
 

We return the BookId of the new book as we impement IRequestHandler<int> . It is the auto generated id after saving the new book. This database is seeded by the way via a json file like this :
 
 
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;

namespace CqrsDemoWebApi.Database.Seed
{
    public static class ModelBuilderExtensions
    {
        public static void SeedBooks(this ModelBuilder modelBuilder, string contentRootPath)
        {
            string jsonInput = File.ReadAllText(Path.Combine(contentRootPath, @"Database/Seed/books.json")); 
            var books = JsonConvert.DeserializeObject<Book[]>(jsonInput);

            int bookId = 0;
            foreach (var book in books!)
            {
                bookId++;
                book.BookId = bookId; 
            }
           
            modelBuilder.Entity<Book>().HasData(books);
            modelBuilder.Entity<Book>().HasKey(b => b.BookId);
            modelBuilder.Entity<Book>().Property(b => b.BookId).ValueGeneratedOnAdd();                 
        }
    }
}
 
 
And we have the DbContext like this:
 
 
 using CqrsDemoWebApi.Database.Seed;
using Microsoft.EntityFrameworkCore;

namespace CqrsDemoWebApi.Database
{
    public class BooksDbContext : DbContext
    {
        private readonly IWebHostEnvironment _hostingEnvironment;

        public BooksDbContext(DbContextOptions<BooksDbContext> options,
            IWebHostEnvironment hostingEnvironment) : base(options) 
        {
           
            _hostingEnvironment = hostingEnvironment;
        }

        public DbSet<Book> Books { get; set; } = null!;  

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            string contentRootPath = _hostingEnvironment.ContentRootPath;
            modelBuilder.SeedBooks(contentRootPath); 
        }
    }
}
 
 
Now, the naming of each nested command/command handler or query/queryhandler is given a generic name. It could be that you want to make this a more specific name, but the wrapping class is a good enough specifier for this. I.e. GetBooks.Query() or AddNewBook.Command (we use [FromBody] here and there do not new up an instance like the Query) are specified enough allowing us to use generic nested class names.. MediatR simplifies the goal of implementing CQRS and following the mediator pattern. There are more possibilities with MediatR. Look at videos on Youtube for this topic for example.

Tuesday 5 July 2022

UnitOfWork pattern in Entity Framework

This article will present code regarding the 'Unit of Work' pattern in Entity Framework, EF. I will also show updated code for a generic repository pattern of the same code. Note that the code in this article is very general and can be run against your DbContext regardless of how it looks. It is solved general, using generics and some usage of reflection. Note that this code goes against DbContext and is written in a solution that uses Entity Framework 6.4.4. However, the code here should work also in newer EF versions, that is - EF Core versions - such as EF Core 6, as the code is very general and EF Core also got the similar structure at least concerning the code shown here.. First off, the code for the interface IUnitOfWork looks like this:
 
 IUnitOfWork.cs

using SomeAcme.Common.Interfaces;

namespace SomeAcme.Data.EntityFramework.Managers
{
    public interface IUnitOfWork
    {
        UnitOfWork AddRepository<T>() where T : class;
         UnitOfWork AddCustomRepository<T>() where T : class;
        int Complete();
        void Dispose();
        UnitOfWork RemoveRepository<T>() where T : class;
        IRepository<T> Repository<T>() where T : class;
    }
}
 
The method Complete is important, as it will commit the transaction and perform changes to the database which the UnitOfWork implementation will work against. The code for UnitWork implementation looks like this:
 
 UnitOfWork.cs

using SomeAcme.Common.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SomeAcme.Data.EntityFramework.Managers
{

    public class UnitOfWork : IUnitOfWork, IDisposable
    {

        private readonly System.Data.Entity.DbContext _dbContext;

        public UnitOfWork(System.Data.Entity.DbContext dbContext)
        {
            _dbContext = dbContext;
            _repositories = new Dictionary<Type, object>();
        }

        public UnitOfWork AddRepository<T>() where T : class
        {
            if (!_repositories.ContainsKey(typeof(T)))
            {
                var repoObj = Activator.CreateInstance(typeof(Repository<T>), _dbContext);
                Repository<T> repo = repoObj as Repository<T>;
                if (repo == null)
                {
                    throw new ArgumentNullException($"Could not instantiate repository of type {typeof(T).Name}");
                }
                _repositories[typeof(T)] = repo;
            }
            return this;
        }

        public UnitOfWork AddCustomRepository<T>() where T : class
        {
            if (!_repositories.ContainsKey(typeof(T)))
            {
                bool checkImpementationPassesGenericInterfaceCheck = typeof(T).GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepository<>));
                if (!checkImpementationPassesGenericInterfaceCheck)
                {
                    throw new ArgumentException($"The type {typeof(T).Name} must implement IRepository<T> to be added as a custom repository");
                }
                var repoObj = Activator.CreateInstance(typeof(T), _dbContext);

                if (repoObj == null)
                {
                    throw new ArgumentNullException($"Could not instantiate repository of type {typeof(T).Name}");
                }
                _repositories[typeof(T)] = repoObj;
            }
            return this;
        }

        public UnitOfWork RemoveRepository<T>() where T : class
        {
            if (_repositories.ContainsKey(typeof(T)))
            {
                _repositories.Remove(typeof(T));
            }
            return this;
        }

        public IRepository<T> Repository<T>() where T : class
        {
            //find suitable repo - possibly a custom repo ..
            IRepository<T> repoFound = null; 

            foreach (var item in _repositories)
            {
                if (item.Key == typeof(T))
                {
                    repoFound = _repositories[typeof(T)] as IRepository<T>;
                    break; 
                }
                bool checkImpementationPassesGenericInterfaceCheck = item.Key.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRepository<>));
                if (checkImpementationPassesGenericInterfaceCheck)
                {
                    //the repo implements IRepository<T> - this is the one to use
                    repoFound = item.Value as IRepository<T>;
                    break;
                }
            }
            if (repoFound == null)
            {
                throw new ArgumentNullException($"Could not retrieve repositiory defined inside the UnitOfWork for entity of type: {typeof(T).Name}. Is it registered into the UnitOfWork. Use method 'AddRepository'");
            }
            return repoFound; 
        }

        private readonly IDictionary<Type, object> _repositories;

        public int Complete()
        {
            if (_dbContext == null)
            {
                throw new ArgumentNullException($"The db context object of the UnitOfWork class is null, cannot complete the UnitOfWork as the db context is not initialized! No changes was performed in DB !"); 
            }
            int numStateEntriesWritten = _dbContext.SaveChanges();
            return numStateEntriesWritten;
        }

        public void Dispose()
        {
            _dbContext?.Dispose(); //dispose the passed in _shared_ db context instead of 
            //disposing the db context inside each repository to dispose only once.. 
            GC.SuppressFinalize(this);
        }

    }
}

 
Note - this implementation focuses on being able to define repositories to the UnitOfWork before running the unit work in case you want to be able to specify which tables / entities got repositories in the db context. Many other implementations will specify the repositories to expose in the UnitOfWork via property getters for example. This will make it easier to implement such a UnitOfWork implementation without this ability to expanding which repositories the UnitOfWork is supporting. Also note that in case you want to add a custom repository that implements IRepository<T> you need to cast into that custom repository when you call the 'Repository' method. A closed abstraction instead, where you do not add repositories like in this code may be more desirable. But perhaps there are scenarios where you want to add repositories that can take part in the UnitOfWork dynamically as shown here. The downside is that you need to initialize the UnitOfWork in addition to performing the database 'steps' before calling the 'Complete' method. What is the most practical UnitOfWork implementation in many cases most probably will be a closed abstraction where you specify which repositories the UnitOfWork supports and avoid having to add repositories like shown here. I did this as an academic exercise to see if such an implementation was possible. The unit tests passes and it looks okay to implement. Note though that you should probably have some default initialization here, i.e. in the constructor of UnitOfWork specify some default repositories and consider if you want to allow adding or removing repositories in the UnitOfWork class. Also, removing repositories could be considered an anti-pattern, so you could disallow this - only allowing adding custom repositories implementating the IRepository<T> in addition to listing up some implemented repositories. Important - always just pass in ONE db context. Each repository must use the same db context instance so the change tracking works as expected. Also note - the UnitOfWork and repositories implement IDisposable. When UnitOfWork goes out of scope, the db context is disposed. In case you want to add the UnitOfWork as a service in a DI container, remember to set up a scoped instance so it will be disposed. In case you use Singleton - this will cause the db connection to hang around.. the Dispose method is exposed as a public method anyways and can be called to dispose on-demand.. Finally, here are some tests that passes testing out the UnitOfWork together with the generic repository pattern !

 
        [Test] 
        public void UnitOfWorkPerformsExpected()
        {
            var dbContext = GetContext();
            var unitOfWork = new UnitOfWork(dbContext);
     
            unitOfWork.AddRepository<OperationExternalEquipment>()
                .AddRepository<OperationDiagnoseCode>();

            var operationExternalEquipment = new OperationExternalEquipment
            {
                OperationId = 10296,
                EquipmentText = "Stent graft type ABC-123",
                OrderedDate = DateTime.Today
            };

            var operationDiagnoseCode = new OperationDiagnoseCode
            {
                OperationId = 10296,
                DiagnoseCodeId = "A09.9",
                IsCodePreFabricated = true                
            };
            
            //act 
            unitOfWork.Repository<OperationExternalEquipment>().Add(operationExternalEquipment);
            unitOfWork.Repository<OperationDiagnoseCode>().Add(operationDiagnoseCode);
            int savedResult = unitOfWork.Complete();

            savedResult.Should().Be(2); 

            //assert 
            var savedOperationEquipmentsForOperation = unitOfWork.Repository<OperationExternalEquipment>().Find(x => x.OperationId == 10296).ToList(); 
            savedOperationEquipmentsForOperation.Any(x => x.EquipmentText == "Stent graft type ABC-123").Should().BeTrue();

            var savedOperationDiagnoseCodesForOperation = unitOfWork.Repository<OperationDiagnoseCode>().Find(x => x.OperationId == 10296).ToList();
            savedOperationDiagnoseCodesForOperation.Any(x => x?.DiagnoseCodeId == "A09.9").Should().BeTrue();

            //cleanup
            unitOfWork.Repository<OperationExternalEquipment>().Remove(operationExternalEquipment);
            unitOfWork.Repository<OperationDiagnoseCode>().Remove(operationDiagnoseCode);
            savedResult = unitOfWork.Complete();

            savedResult.Should().Be(2);

        }

        [Test]
        public void UnitOfWorkCustomRepoPerformsExpected()
        {
            var dbContext = GetContext();
            var unitOfWork = new UnitOfWork(dbContext);
            unitOfWork.AddCustomRepository<OperationExternalEquipmentCustomRepo>();
            var operationExternalEquipment = new OperationExternalEquipment
            {
                OperationId = 10296,
                EquipmentText = "Stent graft type DEF-456",
                OrderedDate = DateTime.Today
            };         

            //act 
            unitOfWork.Repository<OperationExternalEquipment>().Add(operationExternalEquipment);
       
            int savedResult = unitOfWork.Complete();

            savedResult.Should().Be(1);

            //assert 
            var savedOperationEquipmentsForOperation = unitOfWork.Repository<OperationExternalEquipment>().Find(x => x.OperationId == 10296).ToList(); 
            savedOperationEquipmentsForOperation.Any(x => x.EquipmentText == "Stent graft type DEF-456").Should().BeTrue();

            //check if we can use a custom repo method !
            var equipmentRepo = unitOfWork.Repository<OperationExternalEquipment>() as OperationExternalEquipmentCustomRepo;
            var equipmentTexts = string.Join(",", equipmentRepo.GetEquipmentTexts(10296));
            bool foundText = equipmentTexts.Contains("Stent graft type DEF-456"); 
            foundText.Should().BeTrue();

            //cleanup
            unitOfWork.Repository<OperationExternalEquipment>().Remove(operationExternalEquipment);
            savedResult = unitOfWork.Complete();

            savedResult.Should().Be(1);

        }
 
 
The adjusted implementation of Repository now looks like this - I have renamed many of the methods to be more standard compared to other implementations of the repository pattern demonstrated online in different videos on Youtube for example. The updated interface for repository now looks like this:
 
  IRepository.cs
  
  using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace SomeAcme.Common.Interfaces
{

    /// <summary>
    /// Generic implementation of repository pattern for (should maybe have been implemented a decade ago to save some development time .. :-) to save some code in DAL-layer Data.EntityFramework ) 
    /// </summary>
    /// <typeparam name="T">Entity T (POCO for table)</typeparam>
    public interface IRepository<T> where T : class
    {

        /// <summary>
        /// Performs an insert of an entity
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <param name="keyValues">If set, these key values are used to locate entity in db after the insertion has been performed if specifed by other param for saveImmediate</param>
        /// <param name="saveImmediate">Save immediately in db after adding the entity</param>
        T Add(T entity, bool saveImmediate = false);

        /// <summary>
        /// Performs an insert of multiple entities
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <param name="saveImmediate">Save immediately after adding item in db</param>
        IEnumerable<T> AddRange(IEnumerable<T> entity, bool saveImmediate = false);

        /// <summary>
        /// Saves changes. Commits the data to the database.
        /// </summary>
        /// <param name="dbContext">Db context</param>
        void SaveChanges(object dbContext);

        /// <summary>
        /// Delete an entity specified by <paramref name="keyValues"/> to look up entity
        /// </summary>
        /// <param name="keyValues"></param>
        /// <returns></returns>
        T Remove(bool saveImmediate, params object[] keyValues);

        /// <summary>
        /// Deletes an entity specified by <paramref name="entity"/> from the database
        /// </summary>
        /// <param name="entity"></param>
        void Remove(T entity, bool saveImmediate = false);

        /// <summary>
        /// Removes entities specified by <paramref name="entities"/> from the database
        /// </summary>
        /// <param name="entities"></param>
        void RemoveRange(IEnumerable<T> entities, bool saveImmediate = false);

        /// <summary>
        /// Update <paramref name="entity"/>
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <param name="saveImmediate">Save immediate if set to true</param>
        /// <returns></returns>
        T Update(T entity, bool saveImmediate, params object[] keyValues);

        /// <summary>
        /// Equivalent to a 'GetById' method, but tailored for generic use. 
        /// Retrieves <paramref name="idSelector"/> specified by <paramref name="idValue"/>
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="keyValues">Key values to use to find entity</param>
        T Get(bool asNoTracking = true, params object[] keyValues);

        /// <summary>
        /// Retrieves entities of type <typeparamref name="T"/> via predicate <paramref name="condition"/>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="condition"></param>
        /// <returns></returns>
        /// <param name="asNoTracking">If true, does not track items (less chance of db locks due to turning off change tracking) </param>
        IEnumerable<T> Find(Expression<Func<T, bool>> condition, bool asNoTracking = true);

        /// <summary>
        /// Retrieves an entity of type <typeparamref name="T"/> via predicate <paramref name="condition"/>.
        /// If not found, null is returned.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="condition"></param>
        /// <returns></returns>
        T GetByCondition(Expression<Func<T, bool>> condition);

        /// <summary>
        /// Retrieve all the entities specified by <typeparamref name="T"/>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        /// <param name="asNoTracking">If true, does not track items (less chance of db locks due to turning off change tracking) </param>
        IEnumerable<T> GetAll(bool asNoTracking = true); 

    }
}

 

And the implementation looks like this:
 
 
 using SomeAcme.Common.Interfaces;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;

namespace SomeAcme.Data.EntityFramework.Managers
{
    public class Repository<T> : IRepository<T>, IDisposable where T : class
    {
        private readonly System.Data.Entity.DbContext _dbContext;

        public Repository(System.Data.Entity.DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public T Add(T entity, bool saveImmediate = false)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                var entityDb = dbContext.Set<T>().Add(entity);                
                if (saveImmediate)
                    SaveChanges(dbContext);
                return entityDb;

            }, entity);
        }

        public T Add(T entity, bool saveImmediate = false, params object[] keys)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                dbContext.Entry(obj).State = EntityState.Added;
                if (saveImmediate)
                    SaveChanges(dbContext);
                var entityInDb = dbContext.Set<T>().Find(keys);
                return entityInDb;
            }, entity);
        }

        public IEnumerable<T> AddRange(IEnumerable<T> entities, bool saveImmediate)
        {
            var entitites = _dbContext.Set<T>().AddRange(entities);
            if (saveImmediate)
                SaveChanges(_dbContext);
            return entitites;
        }

        public T Remove(bool saveImmediate = false, params object[] keyValues)
        {
            var entity = _dbContext.Set<T>().Find(keyValues);
            if (entity == null)
                return null;
            var entry = _dbContext.Entry(entity);
            if (entry == null)
                return null;
            entry.State = EntityState.Deleted;
            if (saveImmediate)
                SaveChanges(_dbContext);
            return entity;
        }

        public void Remove(T entity, bool saveImmediate = false)
        {
            _dbContext.Set<T>().Remove(entity);
            if (saveImmediate)
                SaveChanges(_dbContext);
        }

        public void RemoveRange(IEnumerable<T> entities, bool saveImmediate = false)
        {
            _dbContext.Set<T>().RemoveRange(entities);
            if (saveImmediate)
                SaveChanges(_dbContext);
        }

        /// <summary>
        /// Note - requiring here that we have defined primary key(s) on the target tables ! 
        /// </summary>
        /// <param name="keyValues"></param>
        /// <returns></returns>
        public T Get(params object[] keyValues)
        {
            var entity = _dbContext.Set<T>().Find(keyValues);
            _dbContext.Entry(entity).State = EntityState.Detached;
            return entity;
        }

        public IEnumerable<T> GetAll(bool asNoTracking = true)
        {
            return asNoTracking ? _dbContext.Set<T>().AsNoTracking() : _dbContext.Set<T>();
        }

        public IEnumerable<T> Find(Expression<Func<T, bool>> condition, bool asNoTracking = true)
        {
            IQueryable<T> query = asNoTracking ? _dbContext.Set<T>().AsNoTracking() : _dbContext.Set<T>();
            var entities = query.Where(condition);
            return entities;
        }

        public T GetByCondition(Expression<Func<T, bool>> condition)
        {
            IQueryable<T> query = _dbContext.Set<T>().AsNoTracking();
            var entities = query.Where(condition);
            return entities.FirstOrDefault();
        }

        public bool ExistsByCondition(Expression<Func<T, bool>> condition)
        {
            IQueryable<T> query = _dbContext.Set<T>().AsNoTracking();
            return query.Any(condition);

        }

        public T Get(bool asNoTracking, params object[] keyValues)
        {
            var entity = asNoTracking ? _dbContext.Set<T>().AsNoTracking().FirstOrDefault() : _dbContext.Set<T>().Find(keyValues);
            return entity;
        }

        public void SaveChanges(object context)
        {
            var dbContext = context as System.Data.Entity.DbContext;
            if (dbContext == null)
            {
                throw new ArgumentException($"dbContext object inside save method : Must be of type System.Data.Entity.DbContext", nameof(context));
            }
            dbContext.SaveChanges(); 
        }

        public T Update(T entity, bool saveImmediate = false, params object[] keyValues)
        {
            return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
            {
                var entityInDb = dbContext.Set<T>().Find(keyValues);
                if (entityInDb == null)
                    return null; 
                dbContext.Entry(entityInDb).CurrentValues.SetValues(obj);                
                if (saveImmediate)
                {
                    SaveChanges(dbContext);
                }
                return obj;
            }, entity);            
        }

        private T ExecuteQuery(Func<T, System.Data.Entity.DbContext, T> query, T entity)
        {
            T result = query(entity, _dbContext);
            return result;
        }

        public void Dispose()
        {
            Dispose(true); 
        }

        private void Dispose(bool isDisposing)
        {
            if (isDisposing)
            {
                _dbContext?.Dispose();
                GC.SuppressFinalize(this);
            }
        }

    }
}

 
 
Totally, the code of the unit of work and repository pattern is about 300 lines of code combined. It should match a lot of Data Access Layer (DAL) implementations of Entity Framework, possible we could reduce a lot of code here in many projects by following these patterns which are accepted data access patterns defined by the 'Gang of four' way back many decades ago. If I would adjust this code next I would do these modifications :
  • UnitOfWork should only allow adding custom repos in addition to some predefines repos. This should give an overall simplification of UnitOfWork
  • Specific transaction handling should be added, like setting the transaction isolation scope. Also speifically doing rollback in case anything crashes in UnitOfWork
  • Possible add some more shared utility methods inside Repository class if such should be added