diff --git a/PartSource.Api/Controllers/WebhooksController.cs b/PartSource.Api/Controllers/WebhooksController.cs
new file mode 100644
index 0000000..9a09a62
--- /dev/null
+++ b/PartSource.Api/Controllers/WebhooksController.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using PartSource.Data.Dtos;
+using PartSource.Data.Models;
+using PartSource.Services;
+using Ratermania.Shopify.Resources;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace PartSource.Api.Controllers
+{
+ ///
+ /// This endpoint handles Shopify webhooks
+ ///
+ [Route("v2/[controller]")]
+ [ApiController]
+ [ApiExplorerSettings(GroupName = "v2")]
+ public class WebhooksController : BaseApiController
+ {
+ private readonly ShopifyChangelogService _shopifyChangelogService;
+
+ public WebhooksController(ShopifyChangelogService shopifyChangelogService)
+ {
+ _shopifyChangelogService = shopifyChangelogService;
+ }
+
+ [HttpPost]
+ [Route("products/update")]
+ public async Task OnProductUpdate([FromBody]Product product)
+ {
+ await _shopifyChangelogService.AddEntry(product);
+
+ return Ok();
+ }
+ }
+}
diff --git a/PartSource.Api/PartSource.Api.csproj b/PartSource.Api/PartSource.Api.csproj
index 9edd31e..b081ba9 100644
--- a/PartSource.Api/PartSource.Api.csproj
+++ b/PartSource.Api/PartSource.Api.csproj
@@ -28,8 +28,11 @@
+
+
+
diff --git a/PartSource.Api/PartSource.Api.xml b/PartSource.Api/PartSource.Api.xml
index a364bd7..9521af0 100644
--- a/PartSource.Api/PartSource.Api.xml
+++ b/PartSource.Api/PartSource.Api.xml
@@ -106,5 +106,10 @@
OK: The engine with the provided EngineConfigId.
Not Found: No engine was found matching the provided EngineConfigId.
+
+
+ This endpoint handles Shopify webhooks
+
+
diff --git a/PartSource.Api/Startup.cs b/PartSource.Api/Startup.cs
index 7197ffe..1ba5b6a 100644
--- a/PartSource.Api/Startup.cs
+++ b/PartSource.Api/Startup.cs
@@ -6,9 +6,10 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
-using PartSource.Api.Formatters;
-using PartSource.Data;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
using PartSource.Data.AutoMapper;
+using PartSource.Data.Contexts;
using PartSource.Services;
using System.IO;
@@ -30,6 +31,14 @@ namespace PartSource.Api
{
options.OutputFormatters.RemoveType(typeof(StringOutputFormatter));
options.EnableEndpointRouting = false;
+ })
+ .AddNewtonsoftJson(options =>
+ {
+ options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
+ options.SerializerSettings.ContractResolver = new DefaultContractResolver()
+ {
+ NamingStrategy = new SnakeCaseNamingStrategy()
+ };
});
services.AddSwaggerGen(c =>
@@ -46,6 +55,7 @@ namespace PartSource.Api
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddCors(o => o.AddPolicy("Default", builder =>
{
@@ -57,6 +67,9 @@ namespace PartSource.Api
services.AddDbContext(options =>
options.UseSqlServer(Configuration.GetConnectionString("PartSourceDatabase"))
);
+ services.AddDbContext(options =>
+ options.UseSqlServer(Configuration.GetConnectionString("FitmentDatabase"))
+ );
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/PartSource.Api/appsettings.json b/PartSource.Api/appsettings.json
index 77e9658..d92f19a 100644
--- a/PartSource.Api/appsettings.json
+++ b/PartSource.Api/appsettings.json
@@ -1,7 +1,8 @@
{
- "ConnectionStrings": {
- "PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
- },
+ "ConnectionStrings": {
+ "PartSourceDatabase": "Server=tcp:ps-whi.database.windows.net,1433;Initial Catalog=ps-whi-stage;Persist Security Info=False;User ID=ps-whi;Password=9-^*N5dw!6:|.5Q;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;",
+ "FitmentDatabase": "Data Source=localhost;Initial Catalog=WhiFitment;Integrated Security=true"
+ },
"Logging": {
"LogLevel": {
"Default": "Warning"
diff --git a/PartSource.Automation/Jobs/ProcessWhiFitment.cs b/PartSource.Automation/Jobs/ProcessWhiFitment.cs
index f5a8ea3..175d6a4 100644
--- a/PartSource.Automation/Jobs/ProcessWhiFitment.cs
+++ b/PartSource.Automation/Jobs/ProcessWhiFitment.cs
@@ -42,96 +42,38 @@ namespace PartSource.Automation.Jobs
string directory = Path.Combine(_ftpConfiguration.Destination, _seoDataType.ToString().ToLowerInvariant());
DirectoryInfo directoryInfo = new DirectoryInfo(directory);
- ConcurrentQueue files = new ConcurrentQueue(directoryInfo.GetFiles().Where(f => f.Name.EndsWith("csv.gz")).OrderBy(f => f.Length));
+ IEnumerable> fileGroups = directoryInfo.GetFiles().Where(f => f.Name.EndsWith("csv.gz")).GroupBy(x => x.Name.Split('_').Last());
- while (files.Count > 0)
+ foreach (IGrouping fileGroup in fileGroups)
{
- Parallel.For(0, 4, index =>
+ foreach (FileInfo fileInfo in fileGroup)
{
- if (!files.TryDequeue(out FileInfo fileInfo))
- {
- return;
- }
-
- string filename = Decompress(fileInfo);
-
- using DataTable dataTable = new DataTable();
- dataTable.Columns.Add("LineCode", typeof(string));
- dataTable.Columns.Add("PartNumber", typeof(string));
- dataTable.Columns.Add("BaseVehicleId", typeof(int));
- dataTable.Columns.Add("EngineConfigId", typeof(int));
- dataTable.Columns.Add("Position", typeof(string));
- dataTable.Columns.Add("NoteText", typeof(string));
-
- using StreamReader reader = new StreamReader(filename);
- string line = reader.ReadLine(); // Burn the header row
-
try
{
- int skippedLines = 0;
- while (reader.Peek() > 0)
- {
- line = reader.ReadLine();
-
- string[] columns = line.Split("\",\"");
-
- if (columns.Length != 8)
- {
- skippedLines++;
- continue;
- }
-
- for (int i = 0; i < columns.Length; i++)
- {
- columns[i] = columns[i].Replace("\"", string.Empty);
- }
-
- string lineCode = Regex.Replace(columns[0], "[^a-zA-Z0-9]", string.Empty).Trim();
- string partNumber = Regex.Replace(columns[1], "[^a-zA-Z0-9]", string.Empty).Trim();
- string position = columns[7].Trim();
- string noteText = columns[4].Trim();
-
- if (!string.IsNullOrEmpty(lineCode)
- && !string.IsNullOrEmpty(partNumber)
- && int.TryParse(columns[5], out int baseVehicleId)
- && int.TryParse(columns[6], out int engineConfigId))
- {
- dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId, position, noteText });
- }
- }
+ string filename = Decompress(fileInfo);
+ DataTable dataTable = GetDataTable(filename);
string tableName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf('.'));
-
+
_whiSeoService.BulkCopy(_seoDataType, dataTable, tableName);
- if (skippedLines == 0)
- {
- _logger.LogInformation($"Copied {filename} to the database.");
- }
+ _logger.LogInformation($"Copied {fileInfo.Name} to the database.");
- else
- {
- _logger.LogWarning($"Copied {filename} to the database with warnings. {skippedLines} lines contained errors and could not be processed.");
- }
-
- File.Delete(fileInfo.FullName);
- }
-
- catch (Exception ex)
- {
- _logger.LogError($"Failed to copy {filename} to the database.", ex);
- }
-
- try
- {
- reader.Close();
File.Delete(filename);
}
- catch (Exception ex) { }
- });
- }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Failed to write {fileInfo.Name} to the database - {ex.Message}", ex);
+ }
+ }
+ string fitmentTable = fileGroup.Key.Substring(0, fileGroup.Key.IndexOf('.'));
+ _whiSeoService.CreateFitmentTable(fitmentTable);
+
+ _logger.LogInformation($"Created fitment table for part group {fitmentTable}.");
+ }
+
_whiSeoService.CreateFitmentView();
}
@@ -146,5 +88,45 @@ namespace PartSource.Automation.Jobs
return decompressedFile;
}
+
+ private DataTable GetDataTable(string filename)
+ {
+ using DataTable dataTable = new DataTable();
+ dataTable.Columns.Add("LineCode", typeof(string));
+ dataTable.Columns.Add("PartNumber", typeof(string));
+ dataTable.Columns.Add("BaseVehicleId", typeof(int));
+ dataTable.Columns.Add("EngineConfigId", typeof(int));
+ //dataTable.Columns.Add("Position", typeof(string));
+ //dataTable.Columns.Add("NoteText", typeof(string));
+
+ using StreamReader reader = new StreamReader(filename);
+ string line = reader.ReadLine(); // Burn the header row
+
+ while (reader.Peek() > 0)
+ {
+ line = reader.ReadLine();
+
+ string[] columns = line.Split("\",\"");
+ for (int i = 0; i < columns.Length; i++)
+ {
+ columns[i] = columns[i].Replace("\"", string.Empty);
+ }
+
+ string lineCode = Regex.Replace(columns[0], "[^a-zA-Z0-9]", string.Empty).Trim();
+ string partNumber = Regex.Replace(columns[1], "[^a-zA-Z0-9]", string.Empty).Trim();
+ string position = columns[7].Trim();
+ string noteText = columns[4].Trim();
+
+ if (!string.IsNullOrEmpty(lineCode)
+ && !string.IsNullOrEmpty(partNumber)
+ && int.TryParse(columns[5], out int baseVehicleId)
+ && int.TryParse(columns[6], out int engineConfigId))
+ {
+ dataTable.Rows.Add(new object[] { lineCode, partNumber, baseVehicleId, engineConfigId }); //, position, noteText });
+ }
+ }
+
+ return dataTable;
+ }
}
}
\ No newline at end of file
diff --git a/PartSource.Automation/Jobs/SyncronizeProducts.cs b/PartSource.Automation/Jobs/SyncronizeProducts.cs
index f71a04f..10263d5 100644
--- a/PartSource.Automation/Jobs/SyncronizeProducts.cs
+++ b/PartSource.Automation/Jobs/SyncronizeProducts.cs
@@ -1,17 +1,14 @@
-using Ratermania.Automation.Interfaces;
-using PartSource.Automation.Models;
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
-using PartSource.Data;
-using PartSource.Services;
+using PartSource.Data.Contexts;
+using PartSource.Data.Models;
+using Ratermania.Automation.Interfaces;
using Ratermania.Shopify;
using Ratermania.Shopify.Resources;
-using PartSource.Data.Models;
+using System;
+using System.Collections.Generic;
using System.Linq;
-using Microsoft.EntityFrameworkCore;
+using System.Threading.Tasks;
namespace PartSource.Automation.Jobs
{
diff --git a/PartSource.Automation/Jobs/UpdateFitment.cs b/PartSource.Automation/Jobs/UpdateFitment.cs
index cba0dfb..c8eef00 100644
--- a/PartSource.Automation/Jobs/UpdateFitment.cs
+++ b/PartSource.Automation/Jobs/UpdateFitment.cs
@@ -74,7 +74,7 @@ namespace PartSource.Automation.Jobs
bool isFitment = false;
- IList vehicles = _vehicleService.GetVehiclesForPart(importData.PartNumber, importData.LineCode, 255);
+ IList vehicles = _vehicleService.GetVehiclesForPart(importData.PartNumber, importData.LineCode);
//if (vehicles.Count > 250)
//{
@@ -166,9 +166,9 @@ namespace PartSource.Automation.Jobs
tags.AddRange(ymmFitment);
- if (tags.Count > 250)
+ if (tags.Count > 249)
{
- tags = tags.Take(250).ToList();
+ tags = tags.Take(249).ToList();
}
string zzzIsFitment = isFitment
@@ -188,7 +188,7 @@ namespace PartSource.Automation.Jobs
catch (Exception ex)
{
- _logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku}", ex);
+ _logger.LogError($"Failed to updated fitment data for SKU {importData?.VariantSku} - {ex.Message}", ex);
}
}
diff --git a/PartSource.Automation/Jobs/UpdatePricing.cs b/PartSource.Automation/Jobs/UpdatePricing.cs
index 1ee53c9..d5f917f 100644
--- a/PartSource.Automation/Jobs/UpdatePricing.cs
+++ b/PartSource.Automation/Jobs/UpdatePricing.cs
@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging.Abstractions;
-using Ratermania.Automation.Interfaces;
-using PartSource.Automation.Models;
-using PartSource.Data;
+using Microsoft.Extensions.Logging;
+using PartSource.Automation.Services;
+using PartSource.Data.Contexts;
using PartSource.Data.Models;
+using Ratermania.Automation.Interfaces;
using Ratermania.Shopify;
using Ratermania.Shopify.Resources;
using Ratermania.Shopify.Resources.Enums;
@@ -11,8 +11,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
-using PartSource.Automation.Services;
namespace PartSource.Automation.Jobs
{
@@ -46,7 +44,7 @@ namespace PartSource.Automation.Jobs
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get the initial set of products from Shopify.");
-
+
throw;
}
@@ -56,35 +54,45 @@ namespace PartSource.Automation.Jobs
{
if (product.Variants.Length > 0)
{
- Variant variant = product.Variants[0];
- PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault();
+ bool hasUpdate = false;
- if (partPrice == null || !partPrice.Your_Price.HasValue || !partPrice.Compare_Price.HasValue)
+ for (int i = 0; i < product.Variants.Length; i++)
{
- continue;
+ Variant variant = product.Variants[i];
+ PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault();
+
+ if (partPrice == null || !partPrice.Your_Price.HasValue || !partPrice.Compare_Price.HasValue)
+ {
+ continue;
+ }
+
+ if (product.Variants[i].Price.ToString("G29") != partPrice.Your_Price.Value.ToString("G29") || product.Variants[i].CompareAtPrice.ToString("G29") != partPrice.Compare_Price.Value.ToString("G29"))
+ {
+ product.Variants[i].Price = partPrice.Your_Price.Value;
+ product.Variants[i].CompareAtPrice = partPrice.Compare_Price.Value;
+
+ product.PublishedAt = partPrice.Active.Trim().ToUpperInvariant() == "Y" ? (DateTime?)DateTime.Now : null;
+ product.PublishedScope = PublishedScope.Global;
+
+ //Metafield metafield = new Metafield
+ //{
+ // Namespace = "Pricing",
+ // Key = "CorePrice",
+ // Value = partPrice.Core_Price.HasValue ? partPrice.Core_Price.Value.ToString() : "0.00",
+ // ValueType = "string",
+ // OwnerResource = "product",
+ // OwnerId = product.Id
+ //};
+
+ hasUpdate = true;
+ }
}
- if (product.Variants[0].Price != partPrice.Your_Price.Value || product.Variants[0].CompareAtPrice != partPrice.Compare_Price.Value)
+ if (hasUpdate)
{
- product.Variants[0].Price = partPrice.Your_Price.Value;
- product.Variants[0].CompareAtPrice = partPrice.Compare_Price.Value;
-
- product.PublishedAt = partPrice.Active.Trim().ToUpperInvariant() == "Y" ? (DateTime?)DateTime.Now : null;
- product.PublishedScope = PublishedScope.Global;
-
- Metafield metafield = new Metafield
- {
- Namespace = "Pricing",
- Key = "CorePrice",
- Value = partPrice.Core_Price.HasValue ? partPrice.Core_Price.Value.ToString() : "0.00",
- ValueType = "string",
- OwnerResource = "product",
- OwnerId = product.Id
- };
-
try
{
- await _shopifyClient.Metafields.Add(metafield);
+ //await _shopifyClient.Metafields.Add(metafield);
await _shopifyClient.Products.Update(product);
updateCount++;
@@ -92,7 +100,7 @@ namespace PartSource.Automation.Jobs
catch (Exception ex)
{
- _logger.LogWarning(ex, $"Failed to update pricing for SKU {variant.Sku}");
+ _logger.LogWarning(ex, $"Failed to update pricing for product ID {product.Id}");
}
}
}
@@ -100,7 +108,7 @@ namespace PartSource.Automation.Jobs
try
{
- products = await _shopifyClient.Products.GetNext();
+ products = await _shopifyClient.Products.GetNext();
_logger.LogInformation($"Total updated: {updateCount}");
}
diff --git a/PartSource.Automation/Program.cs b/PartSource.Automation/Program.cs
index 3a165aa..2c5b64b 100644
--- a/PartSource.Automation/Program.cs
+++ b/PartSource.Automation/Program.cs
@@ -70,23 +70,23 @@ namespace PartSource.Automation
{
options.HasBaseInterval(new TimeSpan(0, 15, 0))
.HasMaxFailures(5)
- //
- //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- // .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- // .HasDependency()
- // .StartsAt(DateTime.Today.AddHours(8))
- //)
- .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- .StartsAt(DateTime.Parse("2021-04-01 08:00:00"))
- )
- .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- .StartsAt(DateTime.Today.AddHours(26))
- )
+ //
+ //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
+ // .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
+ //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
+ // .HasDependency()
+ // .StartsAt(DateTime.Today.AddHours(8))
+ //)
+ //.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
+ // .StartsAt(DateTime.Parse("2021-04-01 08:00:00"))
+ //)
+ .HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
+ .StartsAt(DateTime.Today.AddHours(26))
+ )
.HasJob(options => options.HasInterval(new TimeSpan(24, 0, 0))
- .HasDependency()
- .StartsAt(DateTime.Today.AddHours(27)
- )
+ .HasDependency()
+ .StartsAt(DateTime.Today.AddHours(27)
+ )
);
//.AddApiServer();
})
diff --git a/PartSource.Automation/Services/WhiSeoService.cs b/PartSource.Automation/Services/WhiSeoService.cs
index bd62a0f..264f972 100644
--- a/PartSource.Automation/Services/WhiSeoService.cs
+++ b/PartSource.Automation/Services/WhiSeoService.cs
@@ -1,4 +1,6 @@
-using Microsoft.Extensions.Configuration;
+#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
+
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PartSource.Automation.Models.Configuration;
using PartSource.Automation.Models.Enums;
@@ -65,20 +67,28 @@ namespace PartSource.Automation.Services
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
-#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
- using SqlCommand command = new SqlCommand($"EXEC AddFitmentTable @tableName = '{tableName}'", connection);
+
+ using SqlCommand command = new SqlCommand($"EXEC CreateFitmentTempTable @tableName = '{tableName}'", connection);
command.ExecuteNonQuery();
-#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
using SqlBulkCopy bulk = new SqlBulkCopy(connection)
{
- DestinationTableName = $"{seoDataType}.{tableName}",
+ DestinationTableName = $"FitmentTemp.{tableName}",
BulkCopyTimeout = 14400
};
bulk.WriteToServer(dataTable);
}
+ public void CreateFitmentTable(string tableName)
+ {
+ using SqlConnection connection = new SqlConnection(_connectionString);
+ connection.Open();
+
+ using SqlCommand command = new SqlCommand($"exec CreateFitmentTable @tableName = '{tableName}'", connection);
+ command.ExecuteNonQuery();
+ }
+
public void CreateFitmentView()
{
using SqlConnection connection = new SqlConnection(_connectionString);
@@ -89,3 +99,4 @@ namespace PartSource.Automation.Services
}
}
}
+#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
\ No newline at end of file
diff --git a/PartSource.Data/PartSourceContext.cs b/PartSource.Data/Contexts/PartSourceContext.cs
similarity index 84%
rename from PartSource.Data/PartSourceContext.cs
rename to PartSource.Data/Contexts/PartSourceContext.cs
index b223dea..1f84836 100644
--- a/PartSource.Data/PartSourceContext.cs
+++ b/PartSource.Data/Contexts/PartSourceContext.cs
@@ -2,9 +2,10 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using PartSource.Data.Converters;
using PartSource.Data.Models;
-namespace PartSource.Data
+namespace PartSource.Data.Contexts
{
public class PartSourceContext : DbContext
{
@@ -29,6 +30,8 @@ namespace PartSource.Data
public DbSet Parts { get; set; }
+ public DbSet ShopifyChangelogs { get; set; }
+
public DbSet Vehicles { get; set; }
public DbSet PartAvailabilities { get; set; }
@@ -55,11 +58,18 @@ namespace PartSource.Data
modelBuilder.Entity().HasKey(p => new { p.Store, p.SKU });
+ modelBuilder.Entity()
+ .Property(s => s.ResourceType)
+ .HasConversion(new TypeToStringConverter());
+
+ modelBuilder.Entity()
+ .Property(s => s.Data)
+ .HasConversion(new ObjectToJsonConverter());
+
foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
{
entityType.Relational().TableName = entityType.ClrType.Name;
}
-
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
diff --git a/PartSource.Data/Converters/ObjectToJsonConverter.cs b/PartSource.Data/Converters/ObjectToJsonConverter.cs
new file mode 100644
index 0000000..339b80f
--- /dev/null
+++ b/PartSource.Data/Converters/ObjectToJsonConverter.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Text;
+
+namespace PartSource.Data.Converters
+{
+ public class ObjectToJsonConverter : ValueConverter