public void TestStopOnFirstBeforeHandlerThatHasAnError(bool stopOnFirst)
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <ExampleDbContext>();
            var logs    = new List <LogOutput>();
            var config  = new GenericEventRunnerConfig
            {
                StopOnFirstBeforeHandlerThatHasAnError = stopOnFirst
            };
            var context = options.CreateAndSeedDbWithDiForHandlers <OrderCreatedHandler>(logs, config);
            {
                var tax = new TaxRate(DateTime.Now, 6);
                context.Add(tax);

                //ATTEMPT
                tax.AddEvent(new EventTestBeforeReturnError());
                tax.AddEvent(new EventTestBeforeReturnError());
                var ex = Assert.Throws <GenericEventRunnerStatusException>(() => context.SaveChanges());

                //VERIFY
                context.StatusFromLastSaveChanges.IsValid.ShouldBeFalse();
                context.StatusFromLastSaveChanges.Errors.Count.ShouldEqual(stopOnFirst ? 1 : 2);
                logs.Count.ShouldEqual(stopOnFirst ? 1 : 4);
            }
        }
Пример #2
0
        public void TestAddBookAddedActionToUpdateDates()
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <ExampleDbContext>();
            var config  = new GenericEventRunnerConfig();

            config.AddActionToRunAfterDetectChanges <ExampleDbContext>(localContext =>
            {
                foreach (var entity in localContext.ChangeTracker.Entries()
                         .Where(e =>
                                e.State == EntityState.Added ||
                                e.State == EntityState.Modified))
                {
                    var tracked = entity.Entity as ICreatedUpdated;
                    tracked?.LogChange(entity.State == EntityState.Added, entity);
                }
            });
            var context = options.CreateAndSeedDbWithDiForHandlers <OrderCreatedHandler>(null, config);
            {
                //ATTEMPT
                var book = Book.CreateBookWithEvent("test");
                context.Add(book);
                context.SaveChanges();
                book.WhenCreatedUtc.Subtract(DateTime.UtcNow).TotalMilliseconds.ShouldBeInRange(-100, 10);

                Thread.Sleep(1000);
                book.ChangeTitle("new title");
                context.SaveChanges();

                //VERIFY
                book.LastUpdatedUtc.Subtract(DateTime.UtcNow).TotalMilliseconds.ShouldBeInRange(-100, 10);
            }
        }
Пример #3
0
        public void TestBookDbContextAlterAuthorOk()
        {
            //SETUP
            var options     = SqliteInMemory.CreateOptions <BookDbContext>();
            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = true
            };

            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();
            var books = context.SeedDatabaseFourBooks();

            //ATTEMPT
            books[0].AuthorsLink.Single().Author.Name = "Test Name";
            context.SaveChanges();

            //VERIFY
            context.ChangeTracker.Clear();
            var readBooks = context.Books.ToList();

            readBooks[0].AuthorsOrdered.ShouldEqual("Test Name");
            readBooks[1].AuthorsOrdered.ShouldEqual("Test Name");
            readBooks[2].AuthorsOrdered.ShouldNotEqual("Test Name");
            readBooks[3].AuthorsOrdered.ShouldNotEqual("Test Name");
        }
        public void TestRegisterEventHandlersOnlyBefore()
        {
            //SETUP
            var services = new ServiceCollection();
            var config   = new GenericEventRunnerConfig();

            //ATTEMPT
            var logs = services.RegisterGenericEventRunner(config,
                                                           Assembly.GetAssembly(typeof(BeforeHandler))
                                                           );

            //VERIFY
            services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler <OrderCreatedEvent>),
                                                    typeof(BeforeHandler),
                                                    ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue();

            foreach (var log in logs)
            {
                _output.WriteLine(log);
            }

            config.NotUsingDuringSaveHandlers.ShouldBeTrue();
            config.NotUsingAfterSaveHandlers.ShouldBeTrue();
            services.Count.ShouldEqual(3);
        }
        public void TestBookDbContextAddReviewConcurrencyOk()
        {
            //SETUP
            var options     = SqliteInMemory.CreateOptions <BookDbContext>();
            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = true
            };

            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();
            var books = context.SeedDatabaseFourBooks();

            //ATTEMPT
            books[0].AddReview(3, "comment", "me");
            context.Database.ExecuteSqlInterpolated(
                $"UPDATE Books SET ReviewsCount = 2 WHERE BookId = {books[0].BookId}");
            try
            {
                context.SaveChanges();
            }
            catch (Exception e)
            {
                var status = e.HandleCacheValuesConcurrency(context);
                status.IsValid.ShouldBeTrue(status.GetAllErrors());
                context.SaveChanges();
            }

            //VERIFY
            context.ChangeTracker.Clear();
            var book = context.Books.Include(x => x.Reviews).Single(x => x.BookId == books[0].BookId);

            book.ReviewsCount.ShouldEqual(3);
            book.ReviewsAverageVotes.ShouldEqual(1);
        }
Пример #6
0
        public void TestRegisterEventHandlersWithConfigTuningOffDuringHandlers()
        {
            //SETUP
            var services = new ServiceCollection();
            var config   = new GenericEventRunnerConfig {
                NotUsingDuringSaveHandlers = true
            };

            //ATTEMPT
            services.RegisterGenericEventRunner(config, Assembly.GetAssembly(typeof(BeforeHandlerThrowsExceptionWithAttribute)));

            //VERIFY
            services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler <EventCircularEvent>),
                                                    typeof(BeforeHandlerCircularEvent),
                                                    ServiceLifetime.Transient), new ServiceDescriptorCompare()).ShouldBeTrue();
            services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler <EventTestExceptionHandlerWithAttribute>),
                                                    typeof(BeforeHandlerThrowsExceptionWithAttribute),
                                                    ServiceLifetime.Scoped), new ServiceDescriptorCompare()).ShouldBeTrue();
            services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler <EventTestAfterExceptionHandler>),
                                                    typeof(AfterHandlerThrowsException),
                                                    ServiceLifetime.Transient), new ServiceDescriptorCompare()).ShouldBeTrue();
            services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandlerAsync <EventDoNothing>),
                                                    typeof(AfterHandlerDoNothingAsync),
                                                    ServiceLifetime.Transient), new ServiceDescriptorCompare()).ShouldBeTrue();

            //During event handlers
            services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler <EventDoNothing>),
                                                    typeof(DuringHandlerDoNothing),
                                                    ServiceLifetime.Transient), new ServiceDescriptorCompare()).ShouldBeFalse();
        }
        public void TestBookDbContextAlterAuthorOk()
        {
            //SETUP
            var options     = SqliteInMemory.CreateOptions <BookDbContext>();
            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = true
            };

            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();
            var books = context.SeedDatabaseFourBooks();

            //ATTEMPT
            books[3].AuthorsLink.Single().Author.Name = "Test Name";
            context.Database.ExecuteSqlInterpolated(
                $"UPDATE Books SET AuthorsOrdered = 'bad name' WHERE BookId = {books[3].BookId}");
            try
            {
                context.SaveChanges();
            }
            catch (Exception e)
            {
                var status = e.HandleCacheValuesConcurrency(context);
                status.IsValid.ShouldBeTrue(status.GetAllErrors());
                context.SaveChanges();
            }

            //VERIFY
            context.ChangeTracker.Clear();
            var readBook = context.Books.Single(x => x.BookId == books[3].BookId);

            readBook.AuthorsOrdered.ShouldEqual("Test Name");
        }
        public void TestAddReviewConcurrencyFixed()
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <SqlEventsDbContext>();
            var config  = new GenericEventRunnerConfig();

            config.RegisterSaveChangesExceptionHandler <SqlEventsDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            var context = options.CreateDbWithDiForHandlers <SqlEventsDbContext, ReviewAddedHandler>(config: config);

            context.Database.EnsureCreated();
            var book = WithEventsEfTestData.CreateDummyBookOneAuthor();

            context.Add(book);
            context.SaveChanges();

            book.AddReview(4, "OK", "me");
            //This simulates adding a review with NumStars of 2 before the AddReview
            context.Database.ExecuteSqlRaw(
                "UPDATE Books SET ReviewsCount = @p0, ReviewsAverageVotes = @p1 WHERE BookId = @p2",
                1, 2, book.BookId);

            //ATTEMPT
            context.SaveChanges();

            //VERIFY
            var foundBook = context.Find <BookWithEvents>(book.BookId);

            foundBook.ReviewsCount.ShouldEqual(2);
            foundBook.ReviewsAverageVotes.ShouldEqual(6.0 / 2.0);
        }
        public void TestRegisterEventHandlersBeforeAlreadyRegisteredThreeProjects()
        {
            //SETUP
            var services = new ServiceCollection();
            var config   = new GenericEventRunnerConfig();

            services.AddTransient <IBeforeSaveEventHandler <OrderCreatedEvent>, BeforeHandler>();

            //ATTEMPT
            var logs = services.RegisterGenericEventRunner(config,
                                                           Assembly.GetAssembly(typeof(BeforeHandler)),
                                                           Assembly.GetAssembly(typeof(DuringHandler)),
                                                           Assembly.GetAssembly(typeof(AfterHandler))
                                                           );

            //VERIFY
            services.Contains(new ServiceDescriptor(typeof(IBeforeSaveEventHandler <OrderCreatedEvent>),
                                                    typeof(BeforeHandler),
                                                    ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue();
            services.Contains(new ServiceDescriptor(typeof(IDuringSaveEventHandler <NewBookEvent>),
                                                    typeof(DuringHandler),
                                                    ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue();
            services.Contains(new ServiceDescriptor(typeof(IAfterSaveEventHandler <OrderReadyToDispatchEvent>),
                                                    typeof(AfterHandler),
                                                    ServiceLifetime.Transient), new ServiceDescriptorIncludeLifeTimeCompare()).ShouldBeTrue();

            foreach (var log in logs)
            {
                _output.WriteLine(log);
            }

            config.NotUsingDuringSaveHandlers.ShouldBeFalse();
            config.NotUsingAfterSaveHandlers.ShouldBeFalse();
            services.Count.ShouldEqual(5);
        }
        public void TestBookDeletedConcurrencyFixed()
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <SqlEventsDbContext>();
            var config  = new GenericEventRunnerConfig();

            config.RegisterSaveChangesExceptionHandler <SqlEventsDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            var context = options.CreateDbWithDiForHandlers <SqlEventsDbContext, ReviewAddedHandler>(config: config);

            context.Database.EnsureCreated();
            var books = WithEventsEfTestData.CreateDummyBooks(2);

            context.AddRange(books);
            context.SaveChanges();

            //ATTEMPT
            books.First().AuthorsLink.Last().Author.ChangeName("New common name");
            //This simulates changing the AuthorsOrdered value
            context.Database.ExecuteSqlRaw("DELETE FROM Books WHERE BookId = @p0", books.First().BookId);

            //ATTEMPT
            context.SaveChanges();

            //VERIFY
            var readBooks = context.Books.ToList();

            readBooks.Count().ShouldEqual(1);
            readBooks.First().AuthorsOrdered.ShouldEqual("Author0001, New common name");
        }
Пример #11
0
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services) //#A
        {
            services.AddControllersWithViews()                     //#B
            //.AddRazorRuntimeCompilation() //This recompile a razor page if you edit it while the app is running
            //Added this because my logs display needs the enum as a string
            .AddJsonOptions(opts =>
            {
                opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });

            var bookAppSettings = Configuration.GetBookAppSettings();

            services.AddSingleton(bookAppSettings);

            //This gets the correct sql connection string based on the BookAppSettings
            var sqlConnection = Configuration.GetCorrectSqlConnection(bookAppSettings);

            //This registers both DbContext. Each MUST have a unique MigrationsHistoryTable for Migrations to work
            services.AddDbContext <BookDbContext>(
                options => options.UseSqlServer(sqlConnection, dbOptions =>
                                                dbOptions.MigrationsHistoryTable("BookMigrationHistoryName")));
            services.AddDbContext <OrderDbContext>(
                options => options.UseSqlServer(sqlConnection, dbOptions =>
                                                dbOptions.MigrationsHistoryTable("OrderMigrationHistoryName")));

            services.AddHttpContextAccessor();

            services.Configure <BookAppSettings>(options =>
                                                 Configuration.GetSection(nameof(BookAppSettings)).Bind(options));
            services.AddSingleton <IMenuBuilder, MenuBuilder>();

            //This registers all the services across all the projects in this application
            var diLogs = services.RegisterAssemblyPublicNonGenericClasses(
                Assembly.GetAssembly(typeof(IDisplayOrdersService)),
                Assembly.GetAssembly(typeof(IPlaceOrderBizLogic)),
                Assembly.GetAssembly(typeof(IPlaceOrderDbAccess)))
                         .AsPublicImplementedInterfaces();

            //BookApp.Books startup
            var test = services.RegisterBooksServices(Configuration);

            //Register EfCore.GenericEventRunner
            var eventConfig = new GenericEventRunnerConfig();

            eventConfig.RegisterSaveChangesExceptionHandler <BookDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            eventConfig.AddActionToRunAfterDetectChanges <BookDbContext>(BookDetectChangesExtensions.ChangeChecker);
            var logs = services.RegisterGenericEventRunner(eventConfig,
                                                           Assembly.GetAssembly(typeof(ReviewAddedHandler)) //SQL cached values event handlers
                                                           );

            //Register EfCoreGenericServices
            services.ConfigureGenericServicesEntities(typeof(BookDbContext))
            .ScanAssemblesForDtos(
                BooksStartupInfo.GenericServiceAssemblies.ToArray()
                ).RegisterGenericServices();

            var softLogs = services.RegisterSoftDelServicesAndYourConfigurations();
        }
Пример #12
0
        public static void RegisterInfrastructureDi(this IServiceCollection services)
        {
            //This provides a SaveChangesExceptionHandler which handles concurrency issues around ReviewsCount and ReviewsAverageVotes
            var config = new GenericEventRunnerConfig();

            config.RegisterSaveChangesExceptionHandler <SqlEventsDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);

            //Because I haven't provided any assemblies this will scan this assembly for event handlers
            services.RegisterGenericEventRunner(config);
        }
Пример #13
0
        public void TestGenericEventRunnerConfigAddActionToRunAfterDetectChangeOk()
        {
            //SETUP
            var options     = SqliteInMemory.CreateOptions <BookDbContext>();
            var eventConfig = new GenericEventRunnerConfig();

            eventConfig.AddActionToRunAfterDetectChanges <BookDbContext>(localContext =>
                                                                         localContext.ChangeChecker());
            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();

            //ATTEMPT
            var books = context.SeedDatabaseFourBooks();

            //VERIFY
            var timeNow = DateTime.UtcNow;

            books.ForEach(x => x.LastUpdatedUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), timeNow));
            books.ForEach(x => x.NotUpdatedYet.ShouldBeTrue());
        }
        public void TestAddSaveChangesExceptionHandlerButStillFailsOnOtherDbExceptions()
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <SqlEventsDbContext>();
            var config  = new GenericEventRunnerConfig();

            config.RegisterSaveChangesExceptionHandler <SqlEventsDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            var context = options.CreateDbWithDiForHandlers <SqlEventsDbContext, ReviewAddedHandler>(config: config);

            context.Database.EnsureCreated();

            var review = new ReviewWithEvents(1, "hello", "Me", Guid.NewGuid());

            context.Add(review);

            //ATTEMPT
            var ex = Assert.Throws <DbUpdateException>(() => context.SaveChanges());

            //VERIFY
            ex.InnerException.Message.ShouldEqual("SQLite Error 19: 'FOREIGN KEY constraint failed'.");
        }
Пример #15
0
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services) //#A
        {
            services.AddControllersWithViews()                     //#B
            //.AddRazorRuntimeCompilation() //This recompile a razor page if you edit it while the app is running
            //Added this because my logs display needs the enum as a string
            .AddJsonOptions(opts =>
            {
                opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });


            var connectionString = Configuration.GetConnectionString("DefaultConnection");

            //This registers both DbContext. Each MUST have a unique MigrationsHistoryTable for Migrations to work
            services.AddDbContext <BookDbContext>(
                options => options.UseSqlServer(connectionString, dbOptions =>
                                                dbOptions.MigrationsHistoryTable("BookMigrationHistoryName")));

            services.AddHttpContextAccessor();

            //ModMod.Books startup
            var test = services.RegisterBooksServices(Configuration);

            //Register EfCore.GenericEventRunner
            var eventConfig = new GenericEventRunnerConfig();

            eventConfig.RegisterSaveChangesExceptionHandler <BookDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            eventConfig.AddActionToRunAfterDetectChanges <BookDbContext>(BookDetectChangesExtensions.ChangeChecker);
            var logs = services.RegisterGenericEventRunner(eventConfig,
                                                           Assembly.GetAssembly(typeof(ReviewAddedHandler)) //SQL cached values event handlers
                                                           );

            //Register EfCoreGenericServices
            services.ConfigureGenericServicesEntities(typeof(BookDbContext))
            .ScanAssemblesForDtos(
                BooksStartupInfo.GenericServiceAssemblies.ToArray()
                ).RegisterGenericServices();

            var softLogs = services.RegisterSoftDelServicesAndYourConfigurations();
        }
Пример #16
0
        public async Task TestUpdateProductStockConcurrencyWithHandlerAsync()
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <ExampleDbContext>();
            var config  = new GenericEventRunnerConfig();

            config.RegisterSaveChangesExceptionHandler <ExampleDbContext>(CatchAndFixConcurrencyException);
            var context = options.CreateAndSeedDbWithDiForHandlers <OrderCreatedHandler>(config: config);
            {
                var stock = context.ProductStocks.OrderBy(x => x.NumInStock).First();

                //ATTEMPT
                stock.NumAllocated = 2;
                context.Database.ExecuteSqlRaw(
                    "UPDATE ProductStocks SET NumAllocated = @p0 WHERE ProductName = @p1", 3, stock.ProductName);
                await context.SaveChangesAsync();

                //VERIFY
                var foundStock = context.Find <ProductStock>(stock.ProductName);
                foundStock.NumAllocated.ShouldEqual(5);
            }
        }
Пример #17
0
        public async Task TestWriteBooksAsyncCheckAddUpdateDates()
        {
            //SETUP
            var fileDir = Path.Combine(TestData.GetTestDataDir());
            var options = SqliteInMemory.CreateOptions <BookDbContext>();

            options.TurnOffDispose();
            var timeNow = DateTime.UtcNow;

            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = true
            };

            eventConfig.AddActionToRunAfterDetectChanges <BookDbContext>(localContext =>
                                                                         localContext.ChangeChecker());
            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();
            await context.SeedDatabaseWithBooksAsync(fileDir); //The LastUpdatedUtc is set via events

            context.ChangeTracker.Clear();

            //ATTEMPT
            var serviceProvider = BuildServiceProvider(options);

            var generator = new BookGenerator(serviceProvider);
            await generator.WriteBooksAsync(fileDir, false, 20, true, default);

            //VERIFY
            var books = context.Books
                        .Include(x => x.Reviews)
                        .Include(x => x.AuthorsLink).ToList();

            books.All(x => x.LastUpdatedUtc >= timeNow).ShouldBeTrue();
            books.SelectMany(x => x.Reviews).All(x => x.LastUpdatedUtc >= timeNow).ShouldBeTrue();
            books.SelectMany(x => x.AuthorsLink).All(x => x.LastUpdatedUtc >= timeNow).ShouldBeTrue();
            options.ManualDispose();
        }
Пример #18
0
        public void TestBookDbContextRemoveReviewCacheUpdatedOk()
        {
            //SETUP
            var options     = SqliteInMemory.CreateOptions <BookDbContext>();
            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = true
            };

            using var context = options.CreateDbWithDiForHandlers <BookDbContext, ReviewAddedHandler>(null, eventConfig);
            context.Database.EnsureCreated();
            var books = context.SeedDatabaseFourBooks();

            //ATTEMPT
            books[3].RemoveReview(1);
            context.SaveChanges();

            //VERIFY
            context.ChangeTracker.Clear();
            var book = context.Books.Include(x => x.Reviews).Single(x => x.BookId == books[3].BookId);

            book.ReviewsCount.ShouldEqual(1);
            book.ReviewsAverageVotes.ShouldEqual(3);
        }
Пример #19
0
        public async Task TestStopOnFirstBeforeHandlerThatHasAnError(bool stopOnFirst)
        {
            //SETUP
            var options = SqliteInMemory.CreateOptions <ExampleDbContext>();
            var config  = new GenericEventRunnerConfig
            {
                StopOnFirstBeforeHandlerThatHasAnError = stopOnFirst
            };
            var context = options.CreateAndSeedDbWithDiForHandlers <OrderCreatedHandler>(config: config);
            {
                var tax = new TaxRate(DateTime.Now, 6);
                context.Add(tax);

                //ATTEMPT
                tax.AddEvent(new EventTestBeforeReturnError());
                tax.AddEvent(new EventTestBeforeReturnError());
                var status = await context.SaveChangesWithStatusAsync();

                //VERIFY
                status.IsValid.ShouldBeFalse();
                status.Errors.Count.ShouldEqual(stopOnFirst ? 1 : 2);
                context.StatusFromLastSaveChanges.Errors.Count.ShouldEqual(stopOnFirst ? 1 : 2);
            }
        }
Пример #20
0
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services) //#A
        {
            services.AddControllersWithViews()                     //#B
            .AddRazorRuntimeCompilation()                          //This recompile a razor page if you edit it while the app is running
            //Added this because my logs display needs the enum as a string
            .AddJsonOptions(opts =>
            {
                opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });

            var bookAppSettings = Configuration.GetBookAppSettings();

            services.AddSingleton(bookAppSettings);

            //This gets the correct sql connection string based on the BookAppSettings
            var sqlConnection = Configuration.GetCorrectSqlConnection(bookAppSettings);

            //This registers both DbContext. Each MUST have a unique MigrationsHistoryTable for Migrations to work
            services.AddDbContext <BookDbContext>(
                options => options.UseSqlServer(sqlConnection, dbOptions =>
                                                dbOptions.MigrationsHistoryTable("BookMigrationHistoryName")));
            services.AddDbContext <OrderDbContext>(
                options => options.UseSqlServer(sqlConnection, dbOptions =>
                                                dbOptions.MigrationsHistoryTable("OrderMigrationHistoryName")));

            var cosmosSettings = Configuration.GetCosmosDbSettings(bookAppSettings);

            if (cosmosSettings != null)
            {
                services.AddDbContext <CosmosDbContext>(options => options.UseCosmos(
                                                            cosmosSettings.ConnectionString,
                                                            cosmosSettings.DatabaseName));
            }
            else
            {
                services.AddSingleton <CosmosDbContext>(_ => null);
            }

            services.AddHttpContextAccessor();

            services.Configure <BookAppSettings>(options =>
                                                 Configuration.GetSection(nameof(BookAppSettings)).Bind(options));
            services.AddSingleton <IMenuBuilder, MenuBuilder>();

            //This registers all the services across all the projects in this application
            var diLogs = services.RegisterAssemblyPublicNonGenericClasses(
                Assembly.GetAssembly(typeof(ICheckFixCacheValuesService)),
                Assembly.GetAssembly(typeof(BookListDto)),
                Assembly.GetAssembly(typeof(IBookToCosmosBookService)),
                Assembly.GetAssembly(typeof(IBookGenerator)),
                Assembly.GetAssembly(typeof(IPlaceOrderBizLogic)),
                Assembly.GetAssembly(typeof(IPlaceOrderDbAccess)),
                Assembly.GetAssembly(typeof(IListBooksCachedService)),
                Assembly.GetAssembly(typeof(ICosmosEfListNoSqlBooksService)),
                Assembly.GetAssembly(typeof(IListBooksService)),
                Assembly.GetAssembly(typeof(IDisplayOrdersService)),
                Assembly.GetAssembly(typeof(IListUdfsBooksService)),
                Assembly.GetAssembly(typeof(IListUdfsBooksService))
                )
                         .AsPublicImplementedInterfaces();

            services.AddHostedService <CheckFixCacheBackground>();

            //Register EfCore.GenericEventRunner
            var eventConfig = new GenericEventRunnerConfig
            {
                NotUsingDuringSaveHandlers = cosmosSettings == null //This stops any attempts to update cosmos db if not turned on
            };

            eventConfig.RegisterSaveChangesExceptionHandler <BookDbContext>(BookWithEventsConcurrencyHandler.HandleCacheValuesConcurrency);
            eventConfig.AddActionToRunAfterDetectChanges <BookDbContext>(BookDetectChangesExtensions.ChangeChecker);
            var logs = services.RegisterGenericEventRunner(eventConfig,
                                                           Assembly.GetAssembly(typeof(ReviewAddedHandler)),    //SQL cached values event handlers
                                                           Assembly.GetAssembly(typeof(BookChangeHandlerAsync)) //Cosmos Db event handlers
                                                           );

            //Register EfCoreGenericServices
            services.ConfigureGenericServicesEntities(typeof(BookDbContext), typeof(OrderDbContext))
            .ScanAssemblesForDtos(
                Assembly.GetAssembly(typeof(BookListDto)),
                Assembly.GetAssembly(typeof(AddReviewDto))
                ).RegisterGenericServices();

            var softLogs = services.RegisterSoftDelServicesAndYourConfigurations();
        }