From e862a6232c6922bf0a10ba968d848de74ac8fd08 Mon Sep 17 00:00:00 2001 From: Valdis Iljuconoks Date: Mon, 18 Nov 2024 23:34:17 +0200 Subject: [PATCH] Added command execution timeout configuration option for EF Core DbContext --- CHANGELOG.md | 4 + .../Startup.cs | 1 + .../Geta.Optimizely.ProductFeed.Csv.csproj | 4 +- .../Geta.Optimizely.ProductFeed.Google.csproj | 4 +- .../Configuration/FeedDescriptor.cs | 37 +++--- .../Configuration/ProductFeedOptions.cs | 2 + .../Geta.Optimizely.ProductFeed.csproj | 4 +- .../Repositories/FeedApplicationDbContext.cs | 37 +++--- .../Repositories/FeedRepository.cs | 123 +++++++++++------- .../Repositories/IFeedRepository.cs | 17 ++- .../ServiceCollectionExtensions.cs | 2 +- 11 files changed, 134 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d191a..2035add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [3.0.2] + +* Added command execution timeout configuration option for EF Core DbContext + ## [3.0.1] * Fixed issue with internationalized domain name (https://github.com/Geta/geta-optimizely-productfeed/issues/14) diff --git a/sandbox/Quicksilver/EPiServer.Reference.Commerce.Site/Startup.cs b/sandbox/Quicksilver/EPiServer.Reference.Commerce.Site/Startup.cs index 44108f8..7a98d40 100644 --- a/sandbox/Quicksilver/EPiServer.Reference.Commerce.Site/Startup.cs +++ b/sandbox/Quicksilver/EPiServer.Reference.Commerce.Site/Startup.cs @@ -124,6 +124,7 @@ public void ConfigureServices(IServiceCollection services) .AddProductFeed(options => { options.ConnectionString = _configuration.GetConnectionString("EPiServerDB"); + options.CommandTimeout = TimeSpan.FromMinutes(1); options.SetEntityMapper(); diff --git a/src/Geta.Optimizely.ProductFeed.Csv/Geta.Optimizely.ProductFeed.Csv.csproj b/src/Geta.Optimizely.ProductFeed.Csv/Geta.Optimizely.ProductFeed.Csv.csproj index 2afff25..6ed4b12 100644 --- a/src/Geta.Optimizely.ProductFeed.Csv/Geta.Optimizely.ProductFeed.Csv.csproj +++ b/src/Geta.Optimizely.ProductFeed.Csv/Geta.Optimizely.ProductFeed.Csv.csproj @@ -2,8 +2,8 @@ net6.0 - 3.0.1 - 3.0.1 + 3.0.2 + 3.0.2 Geta.Optimizely.ProductFeed.Csv Geta Optimizely Csv Product Feed ProductFeed true diff --git a/src/Geta.Optimizely.ProductFeed.Google/Geta.Optimizely.ProductFeed.Google.csproj b/src/Geta.Optimizely.ProductFeed.Google/Geta.Optimizely.ProductFeed.Google.csproj index 3049e8e..a7697f5 100644 --- a/src/Geta.Optimizely.ProductFeed.Google/Geta.Optimizely.ProductFeed.Google.csproj +++ b/src/Geta.Optimizely.ProductFeed.Google/Geta.Optimizely.ProductFeed.Google.csproj @@ -2,8 +2,8 @@ net6.0 - 3.0.1 - 3.0.1 + 3.0.2 + 3.0.2 Geta.Optimizely.ProductFeed.Google Geta Optimizely Google Product Feed ProductFeed true diff --git a/src/Geta.Optimizely.ProductFeed/Configuration/FeedDescriptor.cs b/src/Geta.Optimizely.ProductFeed/Configuration/FeedDescriptor.cs index a56a0ae..51877aa 100644 --- a/src/Geta.Optimizely.ProductFeed/Configuration/FeedDescriptor.cs +++ b/src/Geta.Optimizely.ProductFeed/Configuration/FeedDescriptor.cs @@ -3,34 +3,33 @@ using System; -namespace Geta.Optimizely.ProductFeed.Configuration +namespace Geta.Optimizely.ProductFeed.Configuration; + +public class FeedDescriptor { - public class FeedDescriptor + public FeedDescriptor(string name, string fileName, string mimeType) { - public FeedDescriptor(string name, string fileName, string mimeType) - { - Name = name; - FileName = fileName; - MimeType = mimeType; - } + Name = name; + FileName = fileName; + MimeType = mimeType; + } - public string Name { get; set; } + public string Name { get; set; } - public string FileName { get; set; } + public string FileName { get; set; } - public string MimeType { get; set; } + public string MimeType { get; set; } - public Type Exporter { get; set; } + public Type Exporter { get; set; } - public Type Converter { get; set; } + public Type Converter { get; set; } - public Type Filter { get; set; } + public Type Filter { get; set; } - public Type SiteUrlBuilder { get; set; } + public Type SiteUrlBuilder { get; set; } - public void SetSiteUrlBuilder() where TBuilder : ISiteUrlBuilder - { - SiteUrlBuilder = typeof(TBuilder); - } + public void SetSiteUrlBuilder() where TBuilder : ISiteUrlBuilder + { + SiteUrlBuilder = typeof(TBuilder); } } diff --git a/src/Geta.Optimizely.ProductFeed/Configuration/ProductFeedOptions.cs b/src/Geta.Optimizely.ProductFeed/Configuration/ProductFeedOptions.cs index 517713a..d2c8aeb 100644 --- a/src/Geta.Optimizely.ProductFeed/Configuration/ProductFeedOptions.cs +++ b/src/Geta.Optimizely.ProductFeed/Configuration/ProductFeedOptions.cs @@ -27,6 +27,8 @@ public class ProductFeedOptions : ProductFeedOptions public Type EntityMapper { get; set; } + public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(30); + public void Add(FeedDescriptor feedDescriptor) { Descriptors.Add(feedDescriptor); diff --git a/src/Geta.Optimizely.ProductFeed/Geta.Optimizely.ProductFeed.csproj b/src/Geta.Optimizely.ProductFeed/Geta.Optimizely.ProductFeed.csproj index 1815406..21b18ac 100644 --- a/src/Geta.Optimizely.ProductFeed/Geta.Optimizely.ProductFeed.csproj +++ b/src/Geta.Optimizely.ProductFeed/Geta.Optimizely.ProductFeed.csproj @@ -2,8 +2,8 @@ net6.0 - 3.0.1 - 3.0.1 + 3.0.2 + 3.0.2 Geta.Optimizely.ProductFeed Geta Optimizely Product Feed ProductFeed true diff --git a/src/Geta.Optimizely.ProductFeed/Repositories/FeedApplicationDbContext.cs b/src/Geta.Optimizely.ProductFeed/Repositories/FeedApplicationDbContext.cs index 7c2ee97..eb4a94c 100644 --- a/src/Geta.Optimizely.ProductFeed/Repositories/FeedApplicationDbContext.cs +++ b/src/Geta.Optimizely.ProductFeed/Repositories/FeedApplicationDbContext.cs @@ -1,32 +1,33 @@ // Copyright (c) Geta Digital. All rights reserved. // Licensed under Apache-2.0. See the LICENSE file in the project root for more information +using System; using Geta.Optimizely.ProductFeed.Models; using Microsoft.EntityFrameworkCore; // TODO: test namespace change -namespace Geta.Optimizely.GoogleProductFeed.Repositories -{ - public class FeedApplicationDbContext : DbContext - { - private readonly string _connectionString; +namespace Geta.Optimizely.GoogleProductFeed.Repositories; - public FeedApplicationDbContext(string connectionString) - { - _connectionString = connectionString; - } +public sealed class FeedApplicationDbContext : DbContext +{ + private readonly string _connectionString; - public FeedApplicationDbContext(DbContextOptions options) : base(options) - { - } + public FeedApplicationDbContext(string connectionString, TimeSpan commandTimeout) + { + _connectionString = connectionString; + Database.SetCommandTimeout(commandTimeout); + } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (optionsBuilder.IsConfigured) return; + public FeedApplicationDbContext(DbContextOptions options) : base(options) + { + } - optionsBuilder.UseSqlServer(_connectionString); - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured) return; - public DbSet FeedData { get; set; } + optionsBuilder.UseSqlServer(_connectionString); } + + public DbSet FeedData { get; set; } } diff --git a/src/Geta.Optimizely.ProductFeed/Repositories/FeedRepository.cs b/src/Geta.Optimizely.ProductFeed/Repositories/FeedRepository.cs index 579a799..6eed8f0 100644 --- a/src/Geta.Optimizely.ProductFeed/Repositories/FeedRepository.cs +++ b/src/Geta.Optimizely.ProductFeed/Repositories/FeedRepository.cs @@ -4,75 +4,98 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Geta.Optimizely.GoogleProductFeed.Repositories; using Geta.Optimizely.ProductFeed.Configuration; using Geta.Optimizely.ProductFeed.Models; +using Microsoft.EntityFrameworkCore; -namespace Geta.Optimizely.ProductFeed.Repositories +namespace Geta.Optimizely.ProductFeed.Repositories; + +public class FeedRepository : IFeedRepository { - public class FeedRepository : IFeedRepository + private readonly FeedApplicationDbContext _applicationDbContext; + private readonly IEnumerable _descriptors; + + public FeedRepository(FeedApplicationDbContext applicationDbContext, IEnumerable descriptors) { - private readonly FeedApplicationDbContext _applicationDbContext; - private readonly IEnumerable _descriptors; + _applicationDbContext = applicationDbContext; + _descriptors = descriptors; + } - public FeedRepository(FeedApplicationDbContext applicationDbContext, IEnumerable descriptors) + public FeedEntity GetLatestFeed(Uri siteUri) + { + if (siteUri == null) { - _applicationDbContext = applicationDbContext; - _descriptors = descriptors; + throw new ArgumentNullException(nameof(siteUri)); } - public FeedEntity GetLatestFeed(Uri siteUri) + // we need to do client-side eval because string.Equals with comparison is not supported + var feedContent = _applicationDbContext + .FeedData + .ToList(); + + return feedContent.FirstOrDefault(f => f.Link.Equals(GetAbsoluteUrlWithoutQuery(siteUri).AbsoluteUri.TrimEnd('/'), + StringComparison.InvariantCultureIgnoreCase)); + } + + public void Save(ICollection feedData) + { + if (feedData == null) { - if (siteUri == null) - { - throw new ArgumentNullException(nameof(siteUri)); - } - - // we need to do client-side eval because string.Equals with comparison is not supported - var feedContent = _applicationDbContext - .FeedData - .ToList(); - - return feedContent.FirstOrDefault(f => f.Link.Equals(GetAbsoluteUrlWithoutQuery(siteUri).AbsoluteUri.TrimEnd('/'), - StringComparison.InvariantCultureIgnoreCase)); + return; } - public void Save(ICollection feedData) + var feeds = _applicationDbContext.FeedData.ToList(); + + foreach (var data in feedData) { - if (feedData == null) - { - return; - } - - var feeds = _applicationDbContext.FeedData.ToList(); - - foreach (var data in feedData) - { - var found = feeds.FirstOrDefault(f => f.Link.Equals(data.Link, StringComparison.InvariantCultureIgnoreCase)); - - if (found != null) - { - found.CreatedUtc = DateTime.UtcNow; - found.FeedBytes = data.FeedBytes; - } - else - { - data.CreatedUtc = DateTime.UtcNow; - _applicationDbContext.FeedData.Add(data); - } - - _applicationDbContext.SaveChanges(); - } + PrepareFeedData(feeds, data); + _applicationDbContext.SaveChanges(); } + } - public FeedDescriptor FindDescriptorByUri(Uri siteUri) + public async Task SaveAsync(ICollection feedData, CancellationToken cancellationToken) + { + if (feedData == null) { - var path = GetAbsoluteUrlWithoutQuery(siteUri).AbsolutePath.Trim('/'); + return; + } - return _descriptors.FirstOrDefault(d => d.FileName.Trim('/').Equals(path, StringComparison.InvariantCultureIgnoreCase)); + var feeds = await AsyncEnumerable.ToListAsync(_applicationDbContext.FeedData, cancellationToken); + foreach (var data in feedData) + { + PrepareFeedData(feeds, data); + await _applicationDbContext.SaveChangesAsync(cancellationToken); } + } - private Uri GetAbsoluteUrlWithoutQuery(Uri siteUri) - => new UriBuilder(siteUri) { Query = string.Empty }.Uri; + private void PrepareFeedData(List feeds, FeedEntity data) + { + var found = feeds.FirstOrDefault(f => f.Link.Equals(data.Link, StringComparison.InvariantCultureIgnoreCase)); + + if (found != null) + { + found.CreatedUtc = DateTime.UtcNow; + found.FeedBytes = data.FeedBytes; + } + else + { + data.CreatedUtc = DateTime.UtcNow; + _applicationDbContext.FeedData.Add(data); + } + } + + public FeedDescriptor FindDescriptorByUri(Uri siteUri) + { + var path = GetAbsoluteUrlWithoutQuery(siteUri).AbsolutePath.Trim('/'); + + return _descriptors.FirstOrDefault(d => d.FileName.Trim('/').Equals(path, StringComparison.InvariantCultureIgnoreCase)); + } + + private Uri GetAbsoluteUrlWithoutQuery(Uri siteUri) + { + return new UriBuilder(siteUri) { Query = string.Empty }.Uri; } } diff --git a/src/Geta.Optimizely.ProductFeed/Repositories/IFeedRepository.cs b/src/Geta.Optimizely.ProductFeed/Repositories/IFeedRepository.cs index 894b85e..aaa6c8d 100644 --- a/src/Geta.Optimizely.ProductFeed/Repositories/IFeedRepository.cs +++ b/src/Geta.Optimizely.ProductFeed/Repositories/IFeedRepository.cs @@ -3,17 +3,20 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Geta.Optimizely.ProductFeed.Configuration; using Geta.Optimizely.ProductFeed.Models; -namespace Geta.Optimizely.ProductFeed.Repositories +namespace Geta.Optimizely.ProductFeed.Repositories; + +public interface IFeedRepository { - public interface IFeedRepository - { - FeedEntity GetLatestFeed(Uri siteUri); + FeedEntity GetLatestFeed(Uri siteUri); + + void Save(ICollection feedData); - void Save(ICollection feedData); + Task SaveAsync(ICollection feedData, CancellationToken cancellationToken); - FeedDescriptor FindDescriptorByUri(Uri siteUri); - } + FeedDescriptor FindDescriptorByUri(Uri siteUri); } diff --git a/src/Geta.Optimizely.ProductFeed/ServiceCollectionExtensions.cs b/src/Geta.Optimizely.ProductFeed/ServiceCollectionExtensions.cs index d661f67..c1c98b4 100644 --- a/src/Geta.Optimizely.ProductFeed/ServiceCollectionExtensions.cs +++ b/src/Geta.Optimizely.ProductFeed/ServiceCollectionExtensions.cs @@ -31,7 +31,7 @@ public static IServiceCollection AddProductFeed( services.AddTransient(provider => { var options = provider.GetRequiredService>>(); - return new FeedApplicationDbContext(options.Value.ConnectionString); + return new FeedApplicationDbContext(options.Value.ConnectionString, options.Value.CommandTimeout); }); services.AddTransient();