Were you ever in a situation where you needed a quick, handy database but didn’t want to spend a lot of time connecting everything up? Maybe you just need to test a small database table but importing the entire schema takes ages? Well, that was my case and after managing to get one up and running, I wanted to share with you how I got there. The technologies I’m currently working on are .NET Core v3.1 and using Entity Framework Core v5.0 (Nuget package Microsoft.EntityFrameworkCore v5.0.1). Additionally I also had to install the Nuget package Microsoft.EntityFrameworkCore.InMemory.
The scenario in this case is the following; imagine we have a system that notifies users when a PlayStation 5 is in stock at a seller’s store (it would be a million dollar idea right now 😀). There’s a defined list of PS5 seller (Amazon, Ebay, etc) and a user can select to receive stock notifications from this list of sellers. For the sake of this blogpost there’s no front end technologies but just an API. In fact in this case I create a new Visual Studio solution and selected the ASP.NET Core Web Application template and left the standard API option.
Next thing I did was add a new class library project and this will serve as the data layer. I then added a new model with properties to mimic a database table, and how it would be created by the EF model creation process. Similarly I created another class to act as the database context. These implementation can be found below.
| public partial class tblSellers | |
| { | |
| [DatabaseGenerated(DatabaseGeneratedOption.Identity)] | |
| public int Id { get; set; } | |
| public string Name { get; set; } | |
| public string Url { get; set; } | |
| } |
| public partial class StockNotificationContext : DbContext | |
| { | |
| public StockNotificationContext() | |
| { | |
| } | |
| public StockNotificationContext(DbContextOptions<StockNotificationContext> options) : base(options) | |
| { | |
| } | |
| public virtual DbSet<tblSellers> tblSellers { get; set; } | |
| } |
Then I added another class to generate and build the data. All the individual property methods and randomisation of data aren’t really necessary but it’s a practice that we follow at work and I kind of picked up this good habit. I find them useful when working on unit tests as you can play around with data to satisfy test criteria.
| public class tblSellersBuilder | |
| { | |
| private readonly tblSellers _tblSellers; | |
| private readonly Random _random; | |
| public tblSellersBuilder(Random random = null) | |
| { | |
| _random = random ?? new Random(); | |
| _tblSellers = new tblSellers | |
| { | |
| Id = _random.Next(), | |
| Name = _random.Next().ToString(), | |
| Url = _random.Next().ToString(), | |
| }; | |
| } | |
| public tblSellers Build() | |
| { | |
| return _tblSellers; | |
| } | |
| public tblSellersBuilder WithId(int id) | |
| { | |
| _tblSellers.Id = id; | |
| return this; | |
| } | |
| public tblSellersBuilder WithName(string name) | |
| { | |
| _tblSellers.Name = name; | |
| return this; | |
| } | |
| public tblSellersBuilder WithUrl(string url) | |
| { | |
| _tblSellers.Url = url; | |
| return this; | |
| } | |
| public static void Initialize(StockNotificationContext stockNotificationContext) | |
| { | |
| var listOfSellers = new List<tblSellers>(); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(1).WithName("Amazon").WithUrl("https://www.amazon.co.uk").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(2).WithName("Ebay").WithUrl("https://www.ebay.co.uk").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(3).WithName("Currys PC World").WithUrl("https://www.currys.co.uk").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(4).WithName("Argos").WithUrl("https://www.argos.co.uk").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(5).WithName("Smyths").WithUrl("https://www.smythstoys.com").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(6).WithName("Target").WithUrl("https://www.target.com").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(7).WithName("Best Buy").WithUrl("https://www.bestbuy.com").Build()); | |
| listOfSellers.Add(new tblSellersBuilder().WithId(8).WithName("Walmart").WithUrl("https://www.walmart.com").Build()); | |
| stockNotificationContext.tblSellers.AddRange(listOfSellers); | |
| stockNotificationContext.SaveChanges(); | |
| } | |
| } |
I then registered the database and context in the Startup.cs file, ConfigureServices method. After that I added a call to the data generator class in the Configure service so that when my application loads it would have some data to work with.
| public void ConfigureServices(IServiceCollection services) | |
| { | |
| services.AddControllers(); | |
| services.AddDbContext<StockNotificationContext>(options => options.UseInMemoryDatabase(databaseName: "StockNotification")); | |
| services.AddScoped<IUserSettingsService, UserSettingsService>(); | |
| } | |
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
| { | |
| // this should be here when you create the solution | |
| app.UseHttpsRedirection(); | |
| app.UseRouting(); | |
| app.UseAuthorization(); | |
| app.UseEndpoints(endpoints => | |
| { | |
| endpoints.MapControllers(); | |
| }); | |
| using (var serviceScope = app.ApplicationServices.CreateScope()) | |
| { | |
| var dbContext = serviceScope.ServiceProvider.GetService<StockNotificationContext>(); | |
| tblSellersBuilder.Initialize(dbContext); | |
| } | |
| } |
I created another class library project to act as the business layer. In here I added a new service and a service endpoint to get a list of sellers (the same that I generate on application start up). This service is the same one registered in the Startup.cs above. Finally to wrap it up, I created a new controller in the original API project and referenced the service I have just created to be able to provide the user with the list of sellers. Implementations below. I also created an interface for the service and a DTO to return a list of that instead of the database model but omitting them here to keep it short(ish).
| public class UserSettingsService : IUserSettingsService | |
| { | |
| private readonly StockNotificationContext _dbContext; | |
| public UserSettingsService(StockNotificationContext dbContext) | |
| { | |
| _dbContext = dbContext; | |
| } | |
| public async Task<IEnumerable<Seller>> GetSellers() | |
| { | |
| return await _dbContext.tblSellers | |
| .Select(x => new Seller | |
| { | |
| Id = x.Id, | |
| Name = x.Name, | |
| Url = x.Url | |
| }) | |
| .ToListAsync(); | |
| } | |
| } |
| [ApiController] | |
| [Route("[controller]")] | |
| public class SettingsController : ControllerBase | |
| { | |
| private readonly IUserSettingsService _userSettingsService; | |
| public SettingsController(IUserSettingsService userSettingsService) | |
| { | |
| _userSettingsService = userSettingsService; | |
| } | |
| [HttpGet] | |
| public ObjectResult GetSellers() | |
| { | |
| return Ok(_userSettingsService.GetSellers().Result); | |
| } | |
| } |
And that should be enough to have a database working in memory during runtime! At the beginning of this post I came up with this stock notification scenario and that is tied to another post which I will be writing in the coming days. I will discuss an EF related issue I came across and how to fix it. I will also be putting a link to the entire solution on GitHub. If this post helps and would like to donate a PS5 please get in touch 😀
Until next post,
Bjorn
Pingback: instance of entity type cannot be tracked when unit testing ef core | It's not a bug, it's a feature