include cache update
This commit is contained in:
parent
689276ea7d
commit
a2a6c15808
@ -13,4 +13,4 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
VOLUME /data
|
||||
WORKDIR /App
|
||||
COPY --from=build /App/out .
|
||||
ENTRYPOINT ["dotnet", "SolicitorsApi.dll"]
|
||||
ENTRYPOINT ["dotnet", "Solicitors.Api.dll"]
|
||||
@ -1,4 +1,7 @@
|
||||
<Solution>
|
||||
<Folder Name="/app/">
|
||||
<Project Path="app/Solicitors.Api/Solicitors.Api.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/libs/">
|
||||
<Project Path="libs/Solicitors.CacheBuild/Solicitors.CacheBuild.csproj" />
|
||||
<Project Path="libs/Solicitors.Core/Solicitors.Core.csproj" />
|
||||
@ -8,6 +11,4 @@
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/Solicitors.HtmlParsing.Tests/Solicitors.HtmlParsing.Tests.csproj" />
|
||||
</Folder>
|
||||
<Project Path="SolicitorsApi/SolicitorsApi.csproj" />
|
||||
<Project Path="Tester/Tester.csproj" />
|
||||
</Solution>
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Solicitors.CacheBuild;
|
||||
using Solicitors.Core;
|
||||
using Solicitors.Core.Misc;
|
||||
using Solicitors.Core.Models;
|
||||
using Solicitors.Data;
|
||||
using Solicitors.Data.RepositorySetup.Sqlite;
|
||||
using Solicitors.HtmlParsing;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddCore();
|
||||
builder.Services.AddData(new SqliteDbSetupOptions("demo"));
|
||||
builder.Services.AddCacheBuild();
|
||||
builder.Services.AddHtmlParsing();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseCors();
|
||||
|
||||
app.MapGet(
|
||||
"/conveyancors",
|
||||
(
|
||||
[FromServices] IReadOnlySolicitorService solicitorService,
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery] uint pageNumber = 1,
|
||||
[FromQuery] uint pageSize = 20,
|
||||
[FromQuery] string? nameFilter = null,
|
||||
[FromQuery] decimal minRating = 0,
|
||||
[FromQuery] string[]? cities = null,
|
||||
[FromQuery] string ratingsProvider = "Solicitors.com",
|
||||
[FromQuery] string? orderingType = null) =>
|
||||
{
|
||||
IFilter<Solicitor>? filter = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameFilter))
|
||||
filter = new WrapperFilter<Solicitor>(filter, solicitor => solicitor.Name.Contains(nameFilter));
|
||||
|
||||
if (cities is not null && cities.Length > 0)
|
||||
filter = new WrapperFilter<Solicitor>(filter, solicitor => cities.Any(city => solicitor.Cities.Any(solCity => solCity.Name == city)));
|
||||
|
||||
if (minRating > 0)
|
||||
filter = new WrapperFilter<Solicitor>(
|
||||
filter,
|
||||
solicitor => solicitor.Ratings.Any(rating => (rating.Provider == ratingsProvider) && (rating.Value / rating.Maximum >= minRating / 5.0m)));
|
||||
|
||||
IComparer<Solicitor>? ordering = orderingType switch
|
||||
{
|
||||
"rating-asc" => new RatingComparer(ratingsProvider, true),
|
||||
"rating-desc" => new RatingComparer(ratingsProvider, false),
|
||||
"alphabet-asc" => new NameComparer(true),
|
||||
"alphabet-desc" => new NameComparer(false),
|
||||
_ => null
|
||||
};
|
||||
|
||||
return solicitorService.GetSolicitorSummariesAsync(
|
||||
new Pagination(pageNumber, pageSize),
|
||||
ratingsProvider,
|
||||
filter,
|
||||
ordering,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
);
|
||||
|
||||
app.MapGet(
|
||||
"/cities",
|
||||
(
|
||||
[FromServices] IReadOnlyCitiesService citiesService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
return citiesService.GetAllCitiesAsync(cancellationToken);
|
||||
}
|
||||
);
|
||||
|
||||
app.MapGet(
|
||||
"/conveyancors/{id}",
|
||||
(
|
||||
[FromServices] IReadOnlySolicitorService solicitorService,
|
||||
[FromRoute] Guid id,
|
||||
CancellationToken cancellationToken
|
||||
) => solicitorService.GetSolicitorInfoByIdAsync(id, cancellationToken)
|
||||
);
|
||||
|
||||
app.MapGet(
|
||||
"/ratingsProviders",
|
||||
(
|
||||
[FromServices] IRatingsProviderService service,
|
||||
CancellationToken cancellationToken
|
||||
) =>
|
||||
{
|
||||
return service.GetRatingsProvidersAsync(cancellationToken);
|
||||
}
|
||||
);
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var solicitors = scope.ServiceProvider.GetRequiredService<ISolicitorService>();
|
||||
var first = await solicitors
|
||||
.GetSolicitorSummariesAsync(
|
||||
new Pagination(1, 1),
|
||||
"Solicitors.com",
|
||||
cancellationToken: CancellationToken.None);
|
||||
if (first.Data.Length == 0)
|
||||
await scope.ServiceProvider
|
||||
.GetRequiredService<ISolicitorImporter>()
|
||||
.RunFullImport(CancellationToken.None);
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
||||
public class RatingComparer(string ratingsProvider, bool ascending) : IComparer<Solicitor>
|
||||
{
|
||||
public int Compare(Solicitor? x, Solicitor? y)
|
||||
{
|
||||
var xRating = x?.Ratings?.FirstOrDefault(r => r.Provider == ratingsProvider);
|
||||
var yRating = y?.Ratings?.FirstOrDefault(r => r.Provider == ratingsProvider);
|
||||
|
||||
decimal compare;
|
||||
|
||||
if (xRating is null)
|
||||
compare = yRating is null ? 0 : 1;
|
||||
else if (yRating is null)
|
||||
compare = -1;
|
||||
else
|
||||
{
|
||||
compare = (xRating.Value / xRating.Maximum) - (yRating.Value / yRating.Maximum);
|
||||
|
||||
if (!ascending)
|
||||
compare *= -1;
|
||||
}
|
||||
|
||||
if (compare >= 0)
|
||||
return (int)Math.Ceiling(compare);
|
||||
else
|
||||
return (int)Math.Floor(compare);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class NameComparer(bool ascending) : IComparer<Solicitor>
|
||||
{
|
||||
public int Compare(Solicitor? x, Solicitor? y)
|
||||
{
|
||||
int compare;
|
||||
if (ReferenceEquals(x, y))
|
||||
compare = 0;
|
||||
else if (y is null)
|
||||
compare = 1;
|
||||
else if (x is null)
|
||||
compare = -1;
|
||||
else
|
||||
{
|
||||
compare = string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!ascending)
|
||||
compare *= -1;
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
@SolicitorsApi_HostAddress = http://localhost:5084
|
||||
|
||||
GET {{SolicitorsApi_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Solicitors.HtmlParsing;
|
||||
|
||||
var _html = File.ReadAllText("/var/home/fox/main.html");
|
||||
var services = new ServiceCollection();
|
||||
services.AddHtmlParsing();
|
||||
var parser = services.BuildServiceProvider().GetRequiredService<IHtmlParser>();
|
||||
|
||||
var htmlNodes = parser.ParseHtml(_html).ToArray();
|
||||
Console.ReadKey();
|
||||
@ -1,24 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
|
||||
<HintPath>..\..\..\..\..\..\..\..\var\home\fox\.dotnet\shared\Microsoft.AspNetCore.App\10.0.7\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\libs\Solicitors.HtmlParsing\Solicitors.HtmlParsing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
backend/app/Solicitors.Api/Endpoints/CitiesEndpoints.cs
Normal file
17
backend/app/Solicitors.Api/Endpoints/CitiesEndpoints.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Solicitors.Core;
|
||||
|
||||
namespace Solicitors.Api.Endpoints;
|
||||
|
||||
public static class CitiesEndpoints
|
||||
{
|
||||
public static void MapCities(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet(
|
||||
"/cities",
|
||||
(
|
||||
[FromServices] IReadOnlyCitiesService citiesService,
|
||||
CancellationToken cancellationToken
|
||||
) => citiesService.GetAllCitiesAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Solicitors.Core;
|
||||
|
||||
namespace Solicitors.Api.Endpoints;
|
||||
|
||||
public static class RatingsProvidersEndpoints
|
||||
{
|
||||
public static void MapRatingsProviders(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet(
|
||||
"/ratingsProviders",
|
||||
(
|
||||
[FromServices] IRatingsProviderService service,
|
||||
CancellationToken cancellationToken
|
||||
) => service.GetRatingsProvidersAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
60
backend/app/Solicitors.Api/Endpoints/SolicitorsEndpoints.cs
Normal file
60
backend/app/Solicitors.Api/Endpoints/SolicitorsEndpoints.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Solicitors.Core;
|
||||
using Solicitors.Core.Misc;
|
||||
using Solicitors.Core.Models;
|
||||
using Solicitors.Core.Ordering;
|
||||
|
||||
namespace Solicitors.Api.Endpoints;
|
||||
|
||||
public static class SolicitorsEndpoints
|
||||
{
|
||||
public static void MapSolicitors(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet(
|
||||
"/solicitors",
|
||||
(
|
||||
[FromServices] IReadOnlySolicitorService solicitorService,
|
||||
[FromServices] ISolicitorComparerFactory solicitorComparerFactory,
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery] uint pageNumber = 1,
|
||||
[FromQuery] uint pageSize = 20,
|
||||
[FromQuery] string? nameFilter = null,
|
||||
[FromQuery] decimal minRating = 0,
|
||||
[FromQuery] string[]? cities = null,
|
||||
[FromQuery] string ratingsProvider = "Solicitors.com",
|
||||
[FromQuery] OrderingType? orderingType = null) =>
|
||||
{
|
||||
IFilter<Solicitor>? filter = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameFilter))
|
||||
filter = new WrapperFilter<Solicitor>(filter, solicitor => solicitor.Name.Contains(nameFilter));
|
||||
|
||||
if (cities is not null && cities.Length > 0)
|
||||
filter = new WrapperFilter<Solicitor>(filter, solicitor => cities.Any(city => solicitor.Cities.Any(solCity => solCity.Name == city)));
|
||||
|
||||
if (minRating > 0)
|
||||
filter = new WrapperFilter<Solicitor>(
|
||||
filter,
|
||||
solicitor => solicitor.Ratings.Any(rating => rating.Provider == ratingsProvider && rating.Value / rating.Maximum >= minRating / 5.0m));
|
||||
|
||||
var ordering = solicitorComparerFactory.GetComparer(orderingType, ratingsProvider);
|
||||
|
||||
return solicitorService.GetSolicitorSummariesAsync(
|
||||
new Pagination(pageNumber, pageSize),
|
||||
ratingsProvider,
|
||||
filter,
|
||||
ordering,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
);
|
||||
|
||||
app.MapGet(
|
||||
"/solicitors/{id:guid}",
|
||||
(
|
||||
[FromServices] IReadOnlySolicitorService solicitorService,
|
||||
[FromRoute] Guid id,
|
||||
CancellationToken cancellationToken
|
||||
) => solicitorService.GetSolicitorInfoByIdAsync(id, cancellationToken)
|
||||
);
|
||||
}
|
||||
}
|
||||
41
backend/app/Solicitors.Api/Program.cs
Normal file
41
backend/app/Solicitors.Api/Program.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Solicitors.Api.Endpoints;
|
||||
using Solicitors.CacheBuild;
|
||||
using Solicitors.Core;
|
||||
using Solicitors.Data;
|
||||
using Solicitors.HtmlParsing;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddJsonFile("/data/apiSettings.json", optional: true);
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddCore();
|
||||
builder.Services.AddData(builder.Configuration);
|
||||
builder.Services.AddCacheBuild();
|
||||
builder.Services.Configure<ImportConfiguration>(builder.Configuration.GetSection("Imports"));
|
||||
builder.Services.AddHtmlParsing();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.MapSolicitors();
|
||||
app.MapCities();
|
||||
app.MapRatingsProviders();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
await scope.UseCacheBuildAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
app.Run();
|
||||
@ -5,7 +5,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5084",
|
||||
"applicationUrl": "http://localhost:5254",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@ -14,7 +14,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7128;http://localhost:5084",
|
||||
"applicationUrl": "https://localhost:7226;http://localhost:5254",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@ -15,10 +15,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\libs\Solicitors.CacheBuild\Solicitors.CacheBuild.csproj" />
|
||||
<ProjectReference Include="..\libs\Solicitors.Core\Solicitors.Core.csproj" />
|
||||
<ProjectReference Include="..\libs\Solicitors.Data\Solicitors.Data.csproj" />
|
||||
<ProjectReference Include="..\libs\Solicitors.HtmlParsing\Solicitors.HtmlParsing.csproj" />
|
||||
<ProjectReference Include="..\..\libs\Solicitors.CacheBuild\Solicitors.CacheBuild.csproj" />
|
||||
<ProjectReference Include="..\..\libs\Solicitors.Data\Solicitors.Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
backend/app/Solicitors.Api/Solicitors.Api.http
Normal file
6
backend/app/Solicitors.Api/Solicitors.Api.http
Normal file
@ -0,0 +1,6 @@
|
||||
@Solicitors.Api_HostAddress = http://localhost:5254
|
||||
|
||||
GET {{Solicitors.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Quartz;
|
||||
|
||||
namespace Solicitors.CacheBuild;
|
||||
|
||||
@ -10,6 +12,43 @@ public static class DIExtensions
|
||||
{
|
||||
return services
|
||||
.AddScoped<ISolicitorParser, SolicitorsDotCom.SolicitorParser>()
|
||||
.AddScoped<ISolicitorImporter, SolicitorImporter>();
|
||||
.AddScoped<ISolicitorImporter, SolicitorImporter>()
|
||||
.AddQuartz()
|
||||
.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true);
|
||||
}
|
||||
|
||||
public static async Task UseCacheBuildAsync(this IServiceScope scope, CancellationToken cancellationToken)
|
||||
{
|
||||
var factory = scope.ServiceProvider.GetRequiredService<ISchedulerFactory>();
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<ImportConfiguration>>().Value;
|
||||
var scheduler = await factory.GetScheduler(cancellationToken);
|
||||
|
||||
var importJob = JobBuilder.Create<ImportRunner>()
|
||||
.WithIdentity("ImportJob", "ImportGroup")
|
||||
.Build();
|
||||
|
||||
var importTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("ImportRunner", "ImportGroup")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(builder => builder
|
||||
.WithIntervalInMinutes(options.ImportMinutes)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
|
||||
await scheduler.ScheduleJob(importJob, importTrigger, cancellationToken);
|
||||
|
||||
var staleJob = JobBuilder.Create<StaleDataRemover>()
|
||||
.WithIdentity("StaleDataRemover", "RemoveGroup")
|
||||
.Build();
|
||||
|
||||
var staleTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("StaleDataTrigger", "RemoveGroup")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(builder => builder
|
||||
.WithIntervalInMinutes(options.RemoveMinutes)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
|
||||
await scheduler.ScheduleJob(staleJob, staleTrigger, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Solicitors.CacheBuild;
|
||||
|
||||
public class ImportConfiguration
|
||||
{
|
||||
public int StaleMinutes { get; set; }
|
||||
public int RemoveMinutes { get; set; }
|
||||
public int ImportMinutes { get; set; }
|
||||
}
|
||||
15
backend/libs/Solicitors.CacheBuild/ImportRunner.cs
Normal file
15
backend/libs/Solicitors.CacheBuild/ImportRunner.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Quartz;
|
||||
using Solicitors.Core.Data;
|
||||
|
||||
namespace Solicitors.CacheBuild;
|
||||
|
||||
internal class ImportRunner(
|
||||
ISolicitorImporter importer,
|
||||
ISolicitorRepository repository) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
await repository.EnsureCreatedAsync(context.CancellationToken);
|
||||
await importer.RunFullImport(context.CancellationToken);
|
||||
}
|
||||
}
|
||||
@ -3,27 +3,18 @@ using Solicitors.Core.Models.Imports;
|
||||
|
||||
namespace Solicitors.CacheBuild;
|
||||
|
||||
internal class SolicitorImporter : ISolicitorImporter
|
||||
{
|
||||
private readonly ISolicitorParser _parser;
|
||||
private readonly ISolicitorRepository _repository;
|
||||
|
||||
public SolicitorImporter(
|
||||
internal class SolicitorImporter(
|
||||
ISolicitorParser parser,
|
||||
ISolicitorRepository repository)
|
||||
ISolicitorRepository repository) : ISolicitorImporter
|
||||
{
|
||||
_parser = parser;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task RunFullImport(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var solicitor in await _parser.GetSolicitorsAsync(cancellationToken))
|
||||
foreach (var solicitor in await parser.GetSolicitorsAsync(cancellationToken))
|
||||
{
|
||||
await RunImport(solicitor, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private Task RunImport(SolicitorData solicitor, CancellationToken cancellationToken)
|
||||
=> _repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
|
||||
=> repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
|
||||
}
|
||||
@ -13,6 +13,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
||||
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.18.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions">
|
||||
<HintPath>..\..\..\..\..\..\..\.dotnet\shared\Microsoft.AspNetCore.App\10.0.7\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Headers;
|
||||
using Solicitors.Core.Misc;
|
||||
using Solicitors.Core.Models.Imports;
|
||||
using Solicitors.HtmlParsing;
|
||||
@ -154,7 +153,7 @@ internal class SolicitorParser : ISolicitorParser
|
||||
ParseSidebar(sidebarChildren, out phone, out email, out website, out ratings);
|
||||
}
|
||||
|
||||
return new SolicitorData()
|
||||
return new SolicitorData
|
||||
{
|
||||
Name = builder.Name,
|
||||
UrlPath = builder.Path,
|
||||
@ -168,14 +167,14 @@ internal class SolicitorParser : ISolicitorParser
|
||||
};
|
||||
}
|
||||
|
||||
Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
|
||||
private Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
|
||||
=> officeNodes
|
||||
.Select(ParseOffice)
|
||||
.Where(x => x is not null)
|
||||
.Cast<Location>()
|
||||
.ToArray();
|
||||
|
||||
Location? ParseOffice(IHtmlNode officeNode)
|
||||
private Location? ParseOffice(IHtmlNode officeNode)
|
||||
{
|
||||
if (!officeNode.TryGetChildren(out var officeChildren)
|
||||
|| !officeNode.TryGetByTagName("address", out var addressNode)
|
||||
@ -183,7 +182,7 @@ internal class SolicitorParser : ISolicitorParser
|
||||
return null;
|
||||
|
||||
Location? location = null;
|
||||
string? address = "";
|
||||
var address = "";
|
||||
foreach (var line in addressLines)
|
||||
{
|
||||
if (line.TryGetText(out var lineText))
|
||||
@ -214,7 +213,7 @@ internal class SolicitorParser : ISolicitorParser
|
||||
return location;
|
||||
}
|
||||
|
||||
void ParseSidebar(
|
||||
private void ParseSidebar(
|
||||
IEnumerable<IHtmlNode> sidebarChildren,
|
||||
out string? phone,
|
||||
out string? email,
|
||||
@ -253,7 +252,7 @@ internal class SolicitorParser : ISolicitorParser
|
||||
foreach (var child in childrenArray.Where(x => x.TryGetByClass("rev-box", out _, true)))
|
||||
{
|
||||
if (child.TryGetAttributeValue("title", out var ratingString)
|
||||
&& TryParseRatingString(ratingString, out decimal rating, out decimal maxRating)
|
||||
&& TryParseRatingString(ratingString, out var rating, out var maxRating)
|
||||
&& child.TryGetByTagNameAndClass("img", "rev-logo", out var imgNode)
|
||||
&& imgNode.TryGetAttributeValue("src", out var imgSrc)
|
||||
&& TryParseRatingSrc(imgSrc, out var ratingProvider))
|
||||
@ -290,15 +289,15 @@ internal class SolicitorParser : ISolicitorParser
|
||||
return decimal.TryParse(parts[0], out rating) && decimal.TryParse(parts[1], out maxRating);
|
||||
}
|
||||
|
||||
bool TryParseRatingSrc(string imgSrc, out string provider)
|
||||
private bool TryParseRatingSrc(string imgSrc, out string provider)
|
||||
{
|
||||
provider = "";
|
||||
imgSrc = imgSrc.Trim('"');
|
||||
var prefix = "/images/logo-";
|
||||
const string prefix = "/images/logo-";
|
||||
if (!imgSrc.StartsWith(prefix))
|
||||
return false;
|
||||
|
||||
var providerAndExtension = imgSrc.Substring(prefix.Length);
|
||||
var providerAndExtension = imgSrc[prefix.Length..];
|
||||
provider = providerAndExtension.Split('.').First();
|
||||
return true;
|
||||
}
|
||||
@ -306,11 +305,11 @@ internal class SolicitorParser : ISolicitorParser
|
||||
bool TryParseOwnRatingString(string ratingString, out decimal rating)
|
||||
{
|
||||
rating = 0;
|
||||
var prefix = "Average review score : ";
|
||||
const string prefix = "Average review score : ";
|
||||
if (!ratingString.StartsWith(prefix))
|
||||
return false;
|
||||
|
||||
var ratingPart = ratingString.Substring(prefix.Length);
|
||||
var ratingPart = ratingString[prefix.Length..];
|
||||
return decimal.TryParse(ratingPart, out rating);
|
||||
}
|
||||
}
|
||||
19
backend/libs/Solicitors.CacheBuild/StaleDataRemover.cs
Normal file
19
backend/libs/Solicitors.CacheBuild/StaleDataRemover.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Quartz;
|
||||
using Solicitors.Core.Data;
|
||||
|
||||
namespace Solicitors.CacheBuild;
|
||||
|
||||
internal class StaleDataRemover(
|
||||
IOptions<ImportConfiguration> config,
|
||||
ISolicitorRepository repository) : IJob
|
||||
{
|
||||
private readonly ImportConfiguration _config = config.Value;
|
||||
|
||||
public Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
return repository.RemoveStaleEntriesAsync(
|
||||
TimeSpan.FromMinutes(_config.StaleMinutes),
|
||||
context.CancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Solicitors.Core.Ordering;
|
||||
|
||||
namespace Solicitors.Core;
|
||||
|
||||
@ -12,6 +13,7 @@ public static class DIExtensions
|
||||
.AddScoped<ISolicitorService, SolicitorService>()
|
||||
.AddScoped<IReadOnlySolicitorService>(sp => sp.GetRequiredService<ISolicitorService>())
|
||||
.AddScoped<IReadOnlyCitiesService, ReadOnlyCitiesService>()
|
||||
.AddScoped<IRatingsProviderService, RatingsProviderService>();
|
||||
.AddScoped<IRatingsProviderService, RatingsProviderService>()
|
||||
.AddScoped<ISolicitorComparerFactory, SolicitorComparerFactory>();
|
||||
}
|
||||
}
|
||||
@ -10,5 +10,5 @@ public interface IReadOnlySolicitorRepository
|
||||
|
||||
public interface IReadOnlyCitiesRepository
|
||||
{
|
||||
IAsyncEnumerable<string> GetCitiesAsync(CancellationToken cancellationToken);
|
||||
IAsyncEnumerable<string> GetCitiesAsync();
|
||||
}
|
||||
@ -4,5 +4,7 @@ namespace Solicitors.Core.Data;
|
||||
|
||||
public interface ISolicitorRepository : IReadOnlySolicitorRepository
|
||||
{
|
||||
Task EnsureCreatedAsync(CancellationToken cancellationToken);
|
||||
Task AddOrUpdateSolicitorAsync(SolicitorData solicitor, CancellationToken cancellationToken);
|
||||
Task RemoveStaleEntriesAsync(TimeSpan staleAge, CancellationToken cancellationToken);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using Solicitors.Core.Misc;
|
||||
using Solicitors.Core.Models;
|
||||
using Solicitors.Core.Models.View;
|
||||
|
||||
namespace Solicitors.Core;
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
namespace Solicitors.Core;
|
||||
|
||||
public interface ISolicitorService : IReadOnlySolicitorService
|
||||
{
|
||||
|
||||
}
|
||||
public interface ISolicitorService : IReadOnlySolicitorService;
|
||||
@ -1,29 +1,19 @@
|
||||
namespace Solicitors.Core.Misc;
|
||||
|
||||
public interface IFilter<T>
|
||||
public interface IFilter<in T>
|
||||
{
|
||||
bool Filter(T item);
|
||||
}
|
||||
|
||||
public class WrapperFilter<T> : IFilter<T>
|
||||
{
|
||||
private readonly Func<T, bool> _filter;
|
||||
private readonly IFilter<T>? _inner;
|
||||
|
||||
public WrapperFilter(
|
||||
public class WrapperFilter<T>(
|
||||
IFilter<T>? inner,
|
||||
Func<T, bool> filter)
|
||||
Func<T, bool> filter) : IFilter<T>
|
||||
{
|
||||
_inner = inner;
|
||||
_filter = filter;
|
||||
}
|
||||
|
||||
|
||||
public bool Filter(T item)
|
||||
{
|
||||
if (_inner is not null)
|
||||
return _inner.Filter(item) && _filter(item);
|
||||
if (inner is not null)
|
||||
return inner.Filter(item) && filter(item);
|
||||
|
||||
return _filter(item);
|
||||
return filter(item);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ public record City
|
||||
{
|
||||
public Guid CityId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public List<Solicitor> Solicitors { get; } = new();
|
||||
public List<Solicitor> Solicitors { get; } = [];
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
namespace Solicitors.Core.Models.Imports;
|
||||
|
||||
public record Location(string address, string phone, Rating[] locationRatings)
|
||||
public record Location(string Address, string Phone, Rating[] LocationRatings)
|
||||
{
|
||||
public string Address { get; } = address;
|
||||
public string Phone { get; } = phone;
|
||||
public Rating[] LocationRatings { get; } = locationRatings;
|
||||
public string Address { get; } = Address;
|
||||
public string Phone { get; } = Phone;
|
||||
public Rating[] LocationRatings { get; } = LocationRatings;
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
namespace Solicitors.Core.Models.Imports;
|
||||
|
||||
public record Rating(decimal value, decimal maxValue, string provider, string imgSrc)
|
||||
public record Rating(decimal Value, decimal MaxValue, string RatingProvider, string RatingProviderImgSrc)
|
||||
{
|
||||
public decimal Value { get; } = value;
|
||||
public decimal MaxValue { get; } = maxValue;
|
||||
public string RatingProvider { get; } = provider;
|
||||
public string RatingProviderImgSrc { get; } = imgSrc;
|
||||
public decimal Value { get; } = Value;
|
||||
public decimal MaxValue { get; } = MaxValue;
|
||||
public string RatingProvider { get; } = RatingProvider;
|
||||
public string RatingProviderImgSrc { get; } = RatingProviderImgSrc;
|
||||
}
|
||||
@ -11,8 +11,9 @@ public record Solicitor
|
||||
public string? Email { get; set; }
|
||||
public string? Website { get; set; }
|
||||
public string? ShortDescription { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public List<City> Cities { get; } = new();
|
||||
public List<SolicitorRating> Ratings { get; } = new();
|
||||
public List<SolicitorLocation> Locations { get; } = new();
|
||||
public List<City> Cities { get; } = [];
|
||||
public List<SolicitorRating> Ratings { get; } = [];
|
||||
public List<SolicitorLocation> Locations { get; } = [];
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using Solicitors.Core.Models.View;
|
||||
|
||||
namespace Solicitors.Core.Models;
|
||||
|
||||
public record SolicitorInfo
|
||||
@ -25,7 +27,7 @@ public record SolicitorInfo
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string ShortDescription { get; set; }
|
||||
public string? ShortDescription { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
@ -6,8 +6,9 @@ public record SolicitorLocation
|
||||
|
||||
public required string Address { get; set; }
|
||||
public required string Phone { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public List<SolicitorLocationRating> LocationRatings { get; } = new();
|
||||
public List<SolicitorLocationRating> LocationRatings { get; } = [];
|
||||
|
||||
public Guid SolicitorId { get; set; }
|
||||
public required Solicitor Solicitor { get; set; }
|
||||
|
||||
@ -7,6 +7,7 @@ public record SolicitorLocationRating : IRating
|
||||
public required decimal Value { get; set; }
|
||||
public required decimal Maximum { get; set; }
|
||||
public required string Provider { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public Guid SolicitorLocationId { get; set; }
|
||||
public required SolicitorLocation SolicitorLocation { get; set; }
|
||||
|
||||
@ -7,6 +7,7 @@ public record SolicitorRating : IRating
|
||||
public required decimal Value { get; set; }
|
||||
public required decimal Maximum { get; set; }
|
||||
public required string Provider { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
public Guid SolicitorId { get; set; }
|
||||
public required Solicitor Solicitor { get; set; }
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Solicitors.Core.Models;
|
||||
namespace Solicitors.Core.Models.View;
|
||||
|
||||
public record LocationInfo
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Solicitors.Core.Models;
|
||||
namespace Solicitors.Core.Models.View;
|
||||
|
||||
public record RatingInfo
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Solicitors.Core.Models;
|
||||
namespace Solicitors.Core.Models.View;
|
||||
|
||||
public record SolicitorSummary
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
using Solicitors.Core.Models;
|
||||
|
||||
namespace Solicitors.Core.Misc;
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
internal class DefaultSolicitorDataOrdering : IComparer<Solicitor>
|
||||
{
|
||||
@ -0,0 +1,10 @@
|
||||
using Solicitors.Core.Models;
|
||||
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
public interface ISolicitorComparerFactory
|
||||
{
|
||||
IComparer<Solicitor> GetComparer(
|
||||
OrderingType? orderingType,
|
||||
string ratingsProvider);
|
||||
}
|
||||
26
backend/libs/Solicitors.Core/Ordering/NameComparer.cs
Normal file
26
backend/libs/Solicitors.Core/Ordering/NameComparer.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Solicitors.Core.Models;
|
||||
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
internal class NameComparer(bool ascending) : IComparer<Solicitor>
|
||||
{
|
||||
public int Compare(Solicitor? x, Solicitor? y)
|
||||
{
|
||||
int compare;
|
||||
if (ReferenceEquals(x, y))
|
||||
compare = 0;
|
||||
else if (y is null)
|
||||
compare = 1;
|
||||
else if (x is null)
|
||||
compare = -1;
|
||||
else
|
||||
{
|
||||
compare = string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!ascending)
|
||||
compare *= -1;
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
9
backend/libs/Solicitors.Core/Ordering/OrderingType.cs
Normal file
9
backend/libs/Solicitors.Core/Ordering/OrderingType.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
public enum OrderingType
|
||||
{
|
||||
RatingDescending,
|
||||
RatingAscending,
|
||||
AlphabetAscending,
|
||||
AlphabetDescending
|
||||
}
|
||||
32
backend/libs/Solicitors.Core/Ordering/RatingComparer.cs
Normal file
32
backend/libs/Solicitors.Core/Ordering/RatingComparer.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Solicitors.Core.Models;
|
||||
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
internal class RatingComparer(string ratingsProvider, bool ascending) : IComparer<Solicitor>
|
||||
{
|
||||
public int Compare(Solicitor? x, Solicitor? y)
|
||||
{
|
||||
var xRating = x?.Ratings.FirstOrDefault(r => r.Provider == ratingsProvider);
|
||||
var yRating = y?.Ratings.FirstOrDefault(r => r.Provider == ratingsProvider);
|
||||
|
||||
decimal compare;
|
||||
|
||||
if (xRating is null)
|
||||
compare = yRating is null ? 0 : 1;
|
||||
else if (yRating is null)
|
||||
compare = -1;
|
||||
else
|
||||
{
|
||||
compare = (xRating.Value / xRating.Maximum) - (yRating.Value / yRating.Maximum);
|
||||
|
||||
if (!ascending)
|
||||
compare *= -1;
|
||||
}
|
||||
|
||||
if (compare >= 0)
|
||||
return (int)Math.Ceiling(compare);
|
||||
else
|
||||
return (int)Math.Floor(compare);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using Solicitors.Core.Models;
|
||||
|
||||
namespace Solicitors.Core.Ordering;
|
||||
|
||||
internal class SolicitorComparerFactory : ISolicitorComparerFactory
|
||||
{
|
||||
public IComparer<Solicitor> GetComparer(
|
||||
OrderingType? orderingType,
|
||||
string ratingsProvider)
|
||||
{
|
||||
return orderingType switch
|
||||
{
|
||||
OrderingType.RatingAscending => new RatingComparer(ratingsProvider, true),
|
||||
OrderingType.RatingDescending => new RatingComparer(ratingsProvider, false),
|
||||
OrderingType.AlphabetAscending => new NameComparer(true),
|
||||
OrderingType.AlphabetDescending => new NameComparer(false),
|
||||
_ => DefaultSolicitorDataOrdering.Instance
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,10 @@ using Solicitors.Core.Data;
|
||||
|
||||
namespace Solicitors.Core;
|
||||
|
||||
internal class RatingsProviderService : IRatingsProviderService
|
||||
internal class RatingsProviderService(IRatingsProviderRepository repo) : IRatingsProviderService
|
||||
{
|
||||
private readonly IRatingsProviderRepository _repo;
|
||||
|
||||
public RatingsProviderService(IRatingsProviderRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<string[]> GetRatingsProvidersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await _repo.GetAllRatingsProvidersAsync().ToArrayAsync(cancellationToken);
|
||||
return await repo.GetAllRatingsProvidersAsync().ToArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,11 @@ using Solicitors.Core.Data;
|
||||
|
||||
namespace Solicitors.Core;
|
||||
|
||||
internal class ReadOnlyCitiesService : IReadOnlyCitiesService
|
||||
internal class ReadOnlyCitiesService(IReadOnlyCitiesRepository repo) : IReadOnlyCitiesService
|
||||
{
|
||||
private readonly IReadOnlyCitiesRepository _repo;
|
||||
|
||||
public ReadOnlyCitiesService(IReadOnlyCitiesRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<string[]> GetAllCitiesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var cities = _repo.GetCitiesAsync(cancellationToken);
|
||||
var cities = repo.GetCitiesAsync();
|
||||
return await cities.ToArrayAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,13 @@
|
||||
using Solicitors.Core.Data;
|
||||
using Solicitors.Core.Misc;
|
||||
using Solicitors.Core.Models;
|
||||
using Solicitors.Core.Models.View;
|
||||
using Solicitors.Core.Ordering;
|
||||
|
||||
namespace Solicitors.Core;
|
||||
|
||||
internal class SolicitorService : ISolicitorService
|
||||
internal class SolicitorService(IReadOnlySolicitorRepository repo) : ISolicitorService
|
||||
{
|
||||
private readonly IReadOnlySolicitorRepository _repo;
|
||||
|
||||
public SolicitorService(IReadOnlySolicitorRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task<PaginationResponse<SolicitorSummary>> GetSolicitorSummariesAsync(
|
||||
Pagination pagination,
|
||||
string ratingsProvider,
|
||||
@ -20,14 +15,13 @@ internal class SolicitorService : ISolicitorService
|
||||
IComparer<Solicitor>? ordering = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var allSolicitors = _repo.GetAllSolicitorsAsync();
|
||||
var allSolicitors = repo.GetAllSolicitorsAsync();
|
||||
if (filter is not null)
|
||||
allSolicitors = allSolicitors.Where(filter.Filter);
|
||||
|
||||
var matchingSolicitors = await allSolicitors.ToArrayAsync(cancellationToken);
|
||||
|
||||
if (ordering is null)
|
||||
ordering = DefaultSolicitorDataOrdering.Instance;
|
||||
ordering ??= DefaultSolicitorDataOrdering.Instance;
|
||||
|
||||
var results = matchingSolicitors
|
||||
.OrderBy(x => x, ordering)
|
||||
@ -41,7 +35,7 @@ internal class SolicitorService : ISolicitorService
|
||||
|
||||
public async Task<SolicitorInfo?> GetSolicitorInfoByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var data = await _repo.GetSolicitorByIdAsync(id, cancellationToken);
|
||||
var data = await repo.GetSolicitorByIdAsync(id, cancellationToken);
|
||||
if (data is null)
|
||||
return null;
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Solicitors.Core;
|
||||
using Solicitors.Core.Data;
|
||||
using Solicitors.Data.RepositorySetup;
|
||||
using Solicitors.Data.RepositorySetup.InMemory;
|
||||
@ -12,7 +11,7 @@ namespace Solicitors.Data;
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddData(this IServiceCollection services, IRepoSetupOptions options)
|
||||
public static IServiceCollection AddData(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
return services
|
||||
.AddScoped<SolicitorsRepository>()
|
||||
@ -20,17 +19,30 @@ public static class DIExtensions
|
||||
.AddScoped<IReadOnlySolicitorRepository>(sp => sp.GetRequiredService<ISolicitorRepository>())
|
||||
.AddScoped<IReadOnlyCitiesRepository>(sp => sp.GetRequiredService<SolicitorsRepository>())
|
||||
.AddScoped<IRatingsProviderRepository>(sp => sp.GetRequiredService<SolicitorsRepository>())
|
||||
.AddSingleton(GetSetupService(options));
|
||||
.AddSingleton(GetSetupService(configuration));
|
||||
}
|
||||
|
||||
private static IRepoSetupService GetSetupService(IRepoSetupOptions options)
|
||||
private static IRepoSetupService GetSetupService(IConfiguration configuration)
|
||||
{
|
||||
if (options is InMemoryDbSetupOptions inMemOptions)
|
||||
return new InMemorySetupService(inMemOptions);
|
||||
var options = ReadConfigForOptions(configuration);
|
||||
|
||||
if (options is SqliteDbSetupOptions sqliteOptions)
|
||||
return new SqliteSetupService(sqliteOptions);
|
||||
return options switch
|
||||
{
|
||||
InMemoryDbSetupOptions inMemOptions => new InMemorySetupService(inMemOptions),
|
||||
SqliteDbSetupOptions sqliteOptions => new SqliteSetupService(sqliteOptions),
|
||||
_ => throw new ArgumentException("Options type not supported", nameof(configuration))
|
||||
};
|
||||
}
|
||||
|
||||
throw new ArgumentException("Options type not supported", nameof(options));
|
||||
private static IRepoSetupOptions? ReadConfigForOptions(IConfiguration configuration)
|
||||
{
|
||||
var dbConfig = configuration.GetSection("Database");
|
||||
var dbType = dbConfig["Type"]?.ToLower();
|
||||
return dbType switch
|
||||
{
|
||||
"inmemory" => dbConfig.Get<InMemoryDbSetupOptions>(),
|
||||
"sqlite" => dbConfig.Get<SqliteDbSetupOptions>(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,222 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Solicitors.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Solicitors.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(SolicitorsRepository))]
|
||||
[Migration("20260622195820_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
|
||||
|
||||
modelBuilder.Entity("CitySolicitor", b =>
|
||||
{
|
||||
b.Property<Guid>("CitiesCityId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorsSolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("CitiesCityId", "SolicitorsSolicitorId");
|
||||
|
||||
b.HasIndex("SolicitorsSolicitorId");
|
||||
|
||||
b.ToTable("CitySolicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.City", b =>
|
||||
{
|
||||
b.Property<Guid>("CityId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("CityId");
|
||||
|
||||
b.ToTable("Cities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.Solicitor", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativeUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ShortDescription")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorId");
|
||||
|
||||
b.ToTable("Solicitors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorLocationId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorLocationId");
|
||||
|
||||
b.HasIndex("SolicitorId");
|
||||
|
||||
b.ToTable("SolicitorLocations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocationRating", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorLocationRatingId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Maximum")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Provider")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorLocationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorLocationRatingId");
|
||||
|
||||
b.HasIndex("SolicitorLocationId");
|
||||
|
||||
b.ToTable("SolicitorLocationRatings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorRating", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorRatingId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Maximum")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Provider")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorRatingId");
|
||||
|
||||
b.HasIndex("SolicitorId");
|
||||
|
||||
b.ToTable("SolicitorRatings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CitySolicitor", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.City", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CitiesCityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SolicitorsSolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", "Solicitor")
|
||||
.WithMany("Locations")
|
||||
.HasForeignKey("SolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Solicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocationRating", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.SolicitorLocation", "SolicitorLocation")
|
||||
.WithMany("LocationRatings")
|
||||
.HasForeignKey("SolicitorLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SolicitorLocation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorRating", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", "Solicitor")
|
||||
.WithMany("Ratings")
|
||||
.HasForeignKey("SolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Solicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.Solicitor", b =>
|
||||
{
|
||||
b.Navigation("Locations");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.Navigation("LocationRatings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,172 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Solicitors.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Cities",
|
||||
columns: table => new
|
||||
{
|
||||
CityId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Cities", x => x.CityId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Solicitors",
|
||||
columns: table => new
|
||||
{
|
||||
SolicitorId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RelativeUrl = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Phone = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Website = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ShortDescription = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Solicitors", x => x.SolicitorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CitySolicitor",
|
||||
columns: table => new
|
||||
{
|
||||
CitiesCityId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SolicitorsSolicitorId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CitySolicitor", x => new { x.CitiesCityId, x.SolicitorsSolicitorId });
|
||||
table.ForeignKey(
|
||||
name: "FK_CitySolicitor_Cities_CitiesCityId",
|
||||
column: x => x.CitiesCityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "CityId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CitySolicitor_Solicitors_SolicitorsSolicitorId",
|
||||
column: x => x.SolicitorsSolicitorId,
|
||||
principalTable: "Solicitors",
|
||||
principalColumn: "SolicitorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SolicitorLocations",
|
||||
columns: table => new
|
||||
{
|
||||
SolicitorLocationId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Phone = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SolicitorId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SolicitorLocations", x => x.SolicitorLocationId);
|
||||
table.ForeignKey(
|
||||
name: "FK_SolicitorLocations_Solicitors_SolicitorId",
|
||||
column: x => x.SolicitorId,
|
||||
principalTable: "Solicitors",
|
||||
principalColumn: "SolicitorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SolicitorRatings",
|
||||
columns: table => new
|
||||
{
|
||||
SolicitorRatingId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||
Maximum = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||
Provider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SolicitorId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SolicitorRatings", x => x.SolicitorRatingId);
|
||||
table.ForeignKey(
|
||||
name: "FK_SolicitorRatings_Solicitors_SolicitorId",
|
||||
column: x => x.SolicitorId,
|
||||
principalTable: "Solicitors",
|
||||
principalColumn: "SolicitorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SolicitorLocationRatings",
|
||||
columns: table => new
|
||||
{
|
||||
SolicitorLocationRatingId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||
Maximum = table.Column<decimal>(type: "TEXT", nullable: false),
|
||||
Provider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SolicitorLocationId = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SolicitorLocationRatings", x => x.SolicitorLocationRatingId);
|
||||
table.ForeignKey(
|
||||
name: "FK_SolicitorLocationRatings_SolicitorLocations_SolicitorLocationId",
|
||||
column: x => x.SolicitorLocationId,
|
||||
principalTable: "SolicitorLocations",
|
||||
principalColumn: "SolicitorLocationId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CitySolicitor_SolicitorsSolicitorId",
|
||||
table: "CitySolicitor",
|
||||
column: "SolicitorsSolicitorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SolicitorLocationRatings_SolicitorLocationId",
|
||||
table: "SolicitorLocationRatings",
|
||||
column: "SolicitorLocationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SolicitorLocations_SolicitorId",
|
||||
table: "SolicitorLocations",
|
||||
column: "SolicitorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SolicitorRatings_SolicitorId",
|
||||
table: "SolicitorRatings",
|
||||
column: "SolicitorId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CitySolicitor");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SolicitorLocationRatings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SolicitorRatings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Cities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SolicitorLocations");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Solicitors");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,219 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Solicitors.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Solicitors.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(SolicitorsRepository))]
|
||||
partial class SolicitorsRepositoryModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.9");
|
||||
|
||||
modelBuilder.Entity("CitySolicitor", b =>
|
||||
{
|
||||
b.Property<Guid>("CitiesCityId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorsSolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("CitiesCityId", "SolicitorsSolicitorId");
|
||||
|
||||
b.HasIndex("SolicitorsSolicitorId");
|
||||
|
||||
b.ToTable("CitySolicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.City", b =>
|
||||
{
|
||||
b.Property<Guid>("CityId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("CityId");
|
||||
|
||||
b.ToTable("Cities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.Solicitor", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativeUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ShortDescription")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorId");
|
||||
|
||||
b.ToTable("Solicitors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorLocationId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorLocationId");
|
||||
|
||||
b.HasIndex("SolicitorId");
|
||||
|
||||
b.ToTable("SolicitorLocations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocationRating", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorLocationRatingId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Maximum")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Provider")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorLocationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorLocationRatingId");
|
||||
|
||||
b.HasIndex("SolicitorLocationId");
|
||||
|
||||
b.ToTable("SolicitorLocationRatings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorRating", b =>
|
||||
{
|
||||
b.Property<Guid>("SolicitorRatingId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Maximum")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Provider")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SolicitorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<decimal>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SolicitorRatingId");
|
||||
|
||||
b.HasIndex("SolicitorId");
|
||||
|
||||
b.ToTable("SolicitorRatings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CitySolicitor", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.City", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CitiesCityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SolicitorsSolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", "Solicitor")
|
||||
.WithMany("Locations")
|
||||
.HasForeignKey("SolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Solicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocationRating", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.SolicitorLocation", "SolicitorLocation")
|
||||
.WithMany("LocationRatings")
|
||||
.HasForeignKey("SolicitorLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SolicitorLocation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorRating", b =>
|
||||
{
|
||||
b.HasOne("Solicitors.Core.Models.Solicitor", "Solicitor")
|
||||
.WithMany("Ratings")
|
||||
.HasForeignKey("SolicitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Solicitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.Solicitor", b =>
|
||||
{
|
||||
b.Navigation("Locations");
|
||||
|
||||
b.Navigation("Ratings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Solicitors.Core.Models.SolicitorLocation", b =>
|
||||
{
|
||||
b.Navigation("LocationRatings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,3 @@
|
||||
namespace Solicitors.Data.RepositorySetup;
|
||||
|
||||
public interface IRepoSetupOptions
|
||||
{
|
||||
|
||||
}
|
||||
public interface IRepoSetupOptions;
|
||||
@ -1,6 +1,6 @@
|
||||
namespace Solicitors.Data.RepositorySetup.Sqlite;
|
||||
|
||||
public class SqliteDbSetupOptions(string dbName) : IRepoSetupOptions
|
||||
public class SqliteDbSetupOptions(string dbPath) : IRepoSetupOptions
|
||||
{
|
||||
public string DbName { get; } = dbName;
|
||||
public string DbPath { get; } = dbPath;
|
||||
}
|
||||
@ -6,11 +6,6 @@ internal class SqliteSetupService(SqliteDbSetupOptions config) : IRepoSetupServi
|
||||
{
|
||||
public void OnConfiguring(DbContextOptionsBuilder options)
|
||||
{
|
||||
var folder = Environment.SpecialFolder.MyDocuments;
|
||||
var path = Environment.GetFolderPath(folder);
|
||||
path = Path.Combine(path, "/data");
|
||||
var dbPath = System.IO.Path.Join(path, $"{config.DbName}.db");
|
||||
var dbString = $"Data Source={dbPath}";
|
||||
options.UseSqlite(dbString);
|
||||
options.UseSqlite($"Data Source={config.DbPath}");
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Solicitors.Core;
|
||||
using Solicitors.Core.Data;
|
||||
using Solicitors.Core.Models;
|
||||
using Solicitors.Core.Models.Imports;
|
||||
@ -7,15 +6,9 @@ using Solicitors.Data.RepositorySetup;
|
||||
|
||||
namespace Solicitors.Data;
|
||||
|
||||
internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnlyCitiesRepository, IRatingsProviderRepository
|
||||
internal class SolicitorsRepository(IRepoSetupService setup)
|
||||
: DbContext, ISolicitorRepository, IReadOnlyCitiesRepository, IRatingsProviderRepository
|
||||
{
|
||||
private readonly IRepoSetupService _setup;
|
||||
|
||||
public SolicitorsRepository(IRepoSetupService setup)
|
||||
{
|
||||
_setup = setup;
|
||||
}
|
||||
|
||||
public DbSet<Solicitor> Solicitors { get; set; }
|
||||
public DbSet<SolicitorRating> SolicitorRatings { get; set; }
|
||||
public DbSet<SolicitorLocation> SolicitorLocations { get; set; }
|
||||
@ -23,7 +16,10 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
||||
public DbSet<City> Cities { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> _setup.OnConfiguring(optionsBuilder);
|
||||
=> setup.OnConfiguring(optionsBuilder);
|
||||
|
||||
public Task EnsureCreatedAsync(CancellationToken cancellationToken)
|
||||
=> Database.EnsureCreatedAsync(cancellationToken);
|
||||
|
||||
public IAsyncEnumerable<Solicitor> GetAllSolicitorsAsync()
|
||||
=> Solicitors
|
||||
@ -41,104 +37,7 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
||||
.Include(solicitor => solicitor.Ratings)
|
||||
.FirstOrDefaultAsync(x => x.SolicitorId == id, cancellationToken);
|
||||
|
||||
public async Task AddOrUpdateSolicitorAsync(SolicitorData solicitor, CancellationToken cancellationToken)
|
||||
{
|
||||
var matchingSolicitor = await Solicitors
|
||||
.FirstOrDefaultAsync(
|
||||
item => item.Name == solicitor.Name,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (matchingSolicitor is null)
|
||||
await AddNewSolicitor(solicitor, cancellationToken);
|
||||
else
|
||||
await UpdateExistingSolicitor(solicitor, matchingSolicitor, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task AddNewSolicitor(
|
||||
SolicitorData solicitor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var newSol = new Solicitor()
|
||||
{
|
||||
Name = solicitor.Name,
|
||||
RelativeUrl = solicitor.UrlPath,
|
||||
Phone = solicitor.Phone,
|
||||
Email = solicitor.Email,
|
||||
Website = solicitor.Website,
|
||||
ShortDescription = solicitor.ShortDescription
|
||||
};
|
||||
Solicitors.Add(newSol);
|
||||
|
||||
var cityTasks = solicitor.Cities
|
||||
.Distinct()
|
||||
.Select(async (city) =>
|
||||
{
|
||||
var matchingCity = await Cities.FirstOrDefaultAsync(
|
||||
item => item.Name == city,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (matchingCity is null)
|
||||
{
|
||||
matchingCity = new City() { Name = city };
|
||||
Cities.Add(matchingCity);
|
||||
newSol.Cities.Add(matchingCity);
|
||||
}
|
||||
|
||||
matchingCity.Solicitors.Add(newSol);
|
||||
});
|
||||
await Task.WhenAll(cityTasks);
|
||||
|
||||
foreach (var rating in solicitor.Ratings)
|
||||
{
|
||||
var newRating = new SolicitorRating()
|
||||
{
|
||||
Value = rating.Value,
|
||||
Maximum = rating.MaxValue,
|
||||
Provider = rating.RatingProvider,
|
||||
Solicitor = newSol
|
||||
};
|
||||
SolicitorRatings.Add(newRating);
|
||||
newSol.Ratings.Add(newRating);
|
||||
}
|
||||
|
||||
foreach (var location in solicitor.Offices)
|
||||
{
|
||||
var newLocation = new SolicitorLocation()
|
||||
{
|
||||
Address = location.Address,
|
||||
Phone = location.Phone,
|
||||
Solicitor = newSol
|
||||
};
|
||||
SolicitorLocations.Add(newLocation);
|
||||
newSol.Locations.Add(newLocation);
|
||||
|
||||
foreach (var rating in location.LocationRatings)
|
||||
{
|
||||
var newLocRating = new SolicitorLocationRating()
|
||||
{
|
||||
Value = rating.value,
|
||||
Maximum = rating.MaxValue,
|
||||
Provider = rating.RatingProvider,
|
||||
SolicitorLocation = newLocation
|
||||
};
|
||||
SolicitorLocationRatings.Add(newLocRating);
|
||||
newLocation.LocationRatings.Add(newLocRating);
|
||||
}
|
||||
}
|
||||
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private Task UpdateExistingSolicitor(
|
||||
SolicitorData solicitor,
|
||||
Solicitor matchingSolicitor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<string> GetCitiesAsync(CancellationToken cancellationToken)
|
||||
public IAsyncEnumerable<string> GetCitiesAsync()
|
||||
{
|
||||
return Cities
|
||||
.Select(city => city.Name)
|
||||
@ -154,4 +53,286 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
||||
.ThenBy(x => x)
|
||||
.AsAsyncEnumerable();
|
||||
}
|
||||
|
||||
public async Task AddOrUpdateSolicitorAsync(SolicitorData solicitor, CancellationToken cancellationToken)
|
||||
{
|
||||
var matchingSolicitor = await Solicitors
|
||||
.Include(existing => existing.Cities)
|
||||
.Include(existing => existing.Locations)
|
||||
.ThenInclude(location => location.LocationRatings)
|
||||
.Include(existing => existing.Ratings)
|
||||
.FirstOrDefaultAsync(
|
||||
item => item.Name == solicitor.Name,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (matchingSolicitor is null)
|
||||
await AddNewSolicitor(solicitor, cancellationToken);
|
||||
else
|
||||
UpdateExistingSolicitor(solicitor, matchingSolicitor);
|
||||
|
||||
try
|
||||
{
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task RemoveStaleEntriesAsync(TimeSpan staleAge, CancellationToken cancellationToken)
|
||||
{
|
||||
var staleTime = DateTime.UtcNow - staleAge;
|
||||
|
||||
Solicitors.RemoveRange(Solicitors.Where(x => x.LastModified < staleTime));
|
||||
SolicitorLocations.RemoveRange(SolicitorLocations.Where(x => x.LastModified < staleTime));
|
||||
SolicitorRatings.RemoveRange(SolicitorRatings.Where(x => x.LastModified < staleTime));
|
||||
SolicitorLocationRatings.RemoveRange(SolicitorLocationRatings.Where(x => x.LastModified < staleTime));
|
||||
Cities.RemoveRange(Cities.Where(x => x.LastModified < staleTime));
|
||||
|
||||
return SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task AddNewSolicitor(
|
||||
SolicitorData solicitor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var newSol = new Solicitor
|
||||
{
|
||||
Name = solicitor.Name,
|
||||
RelativeUrl = solicitor.UrlPath
|
||||
};
|
||||
UpdateSolicitorRootData(newSol, solicitor);
|
||||
Solicitors.Add(newSol);
|
||||
|
||||
var cityTasks = solicitor.Cities
|
||||
.Distinct()
|
||||
.Select(async city =>
|
||||
{
|
||||
var matchingCity = await GetCityByNameAsync(city, cancellationToken);
|
||||
|
||||
if (matchingCity is null)
|
||||
AddNewCityToSolicitor(city, newSol);
|
||||
else
|
||||
UpdateExistingCity(matchingCity, newSol);
|
||||
});
|
||||
|
||||
await Task.WhenAll(cityTasks);
|
||||
|
||||
foreach (var rating in solicitor.Ratings)
|
||||
AddNewRatingToSolicitor(rating, newSol);
|
||||
|
||||
foreach (var location in solicitor.Offices)
|
||||
AddNewLocationToSolicitor(location, newSol);
|
||||
}
|
||||
|
||||
private void UpdateSolicitorRootData(Solicitor solicitor, SolicitorData data)
|
||||
{
|
||||
solicitor.Name = data.Name;
|
||||
solicitor.RelativeUrl = data.UrlPath;
|
||||
solicitor.Phone = data.Phone;
|
||||
solicitor.Email = data.Email;
|
||||
solicitor.Website = data.Website;
|
||||
solicitor.ShortDescription = data.ShortDescription;
|
||||
solicitor.LastModified = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void AddNewCityToSolicitor(string cityName, Solicitor solicitor)
|
||||
{
|
||||
var newCity = new City
|
||||
{
|
||||
Name = cityName
|
||||
};
|
||||
Cities.Add(newCity);
|
||||
UpdateExistingCity(newCity, solicitor);
|
||||
}
|
||||
|
||||
private void AddNewRatingToSolicitor(Rating rating, Solicitor solicitor)
|
||||
{
|
||||
var newRating = new SolicitorRating
|
||||
{
|
||||
Value = rating.Value,
|
||||
Maximum = rating.MaxValue,
|
||||
Provider = rating.RatingProvider,
|
||||
Solicitor = solicitor,
|
||||
LastModified = DateTime.UtcNow
|
||||
};
|
||||
SolicitorRatings.Add(newRating);
|
||||
solicitor.Ratings.Add(newRating);
|
||||
}
|
||||
|
||||
private void AddNewLocationToSolicitor(Location location, Solicitor solicitor)
|
||||
{
|
||||
var newLocation = new SolicitorLocation
|
||||
{
|
||||
Address = location.Address,
|
||||
Phone = location.Phone,
|
||||
Solicitor = solicitor,
|
||||
LastModified = DateTime.UtcNow
|
||||
};
|
||||
SolicitorLocations.Add(newLocation);
|
||||
solicitor.Locations.Add(newLocation);
|
||||
|
||||
foreach (var rating in location.LocationRatings)
|
||||
AddNewRatingToLocation(rating, newLocation);
|
||||
}
|
||||
|
||||
private void AddNewRatingToLocation(Rating rating, SolicitorLocation location)
|
||||
{
|
||||
var newLocRating = new SolicitorLocationRating
|
||||
{
|
||||
Value = rating.Value,
|
||||
Maximum = rating.MaxValue,
|
||||
Provider = rating.RatingProvider,
|
||||
SolicitorLocation = location,
|
||||
LastModified = DateTime.UtcNow
|
||||
};
|
||||
SolicitorLocationRatings.Add(newLocRating);
|
||||
location.LocationRatings.Add(newLocRating);
|
||||
}
|
||||
|
||||
private void UpdateExistingSolicitor(
|
||||
SolicitorData solicitor,
|
||||
Solicitor matchingSolicitor)
|
||||
{
|
||||
UpdateSolicitorRootData(matchingSolicitor, solicitor);
|
||||
UpdateExistingCities(solicitor, matchingSolicitor);
|
||||
UpdateExistingRatings(solicitor, matchingSolicitor);
|
||||
UpdateExistingLocations(solicitor, matchingSolicitor);
|
||||
}
|
||||
|
||||
private void UpdateExistingCities(
|
||||
SolicitorData solicitor,
|
||||
Solicitor matchingSolicitor)
|
||||
{
|
||||
var lostCities = matchingSolicitor.Cities
|
||||
.Where(city => solicitor.Cities.All(cityName => cityName != city.Name))
|
||||
.ToArray();
|
||||
|
||||
foreach (var city in lostCities)
|
||||
{
|
||||
matchingSolicitor.Cities.Remove(city);
|
||||
city.Solicitors.Remove(matchingSolicitor);
|
||||
}
|
||||
foreach (var cityName in solicitor.Cities.Distinct())
|
||||
{
|
||||
var matchingCity = matchingSolicitor.Cities
|
||||
.FirstOrDefault(city => cityName == city.Name);
|
||||
|
||||
if (matchingCity is null)
|
||||
AddNewCityToSolicitor(cityName, matchingSolicitor);
|
||||
else
|
||||
UpdateExistingCity(matchingCity, matchingSolicitor);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExistingRatings(
|
||||
SolicitorData solicitor,
|
||||
Solicitor matchingSolicitor)
|
||||
{
|
||||
var lostRatings = matchingSolicitor.Ratings
|
||||
.Where(solicitorRating => solicitor.Ratings.All(rating => rating.RatingProvider != solicitorRating.Provider))
|
||||
.ToArray();
|
||||
|
||||
foreach (var rating in lostRatings)
|
||||
{
|
||||
matchingSolicitor.Ratings.Remove(rating);
|
||||
SolicitorRatings.Remove(rating);
|
||||
}
|
||||
|
||||
foreach (var rating in solicitor.Ratings)
|
||||
{
|
||||
var matchingRating = matchingSolicitor.Ratings
|
||||
.FirstOrDefault(solicitorRating => rating.RatingProvider == solicitorRating.Provider);
|
||||
|
||||
if (matchingRating is null)
|
||||
AddNewRatingToSolicitor(rating, matchingSolicitor);
|
||||
else
|
||||
UpdateExistingRating(rating, matchingRating);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExistingRating(Rating rating, SolicitorRating existingRating)
|
||||
{
|
||||
existingRating.Value = rating.Value;
|
||||
existingRating.Maximum = rating.MaxValue;
|
||||
existingRating.LastModified = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void UpdateExistingLocations(
|
||||
SolicitorData solicitor,
|
||||
Solicitor matchingSolicitor)
|
||||
{
|
||||
var lostLocations = matchingSolicitor.Locations
|
||||
.Where(location => solicitor.Offices.All(office => office.Address != location.Address))
|
||||
.ToArray();
|
||||
|
||||
foreach (var location in lostLocations)
|
||||
{
|
||||
matchingSolicitor.Locations.Remove(location);
|
||||
SolicitorLocations.Remove(location);
|
||||
}
|
||||
|
||||
foreach (var office in solicitor.Offices)
|
||||
{
|
||||
var location = matchingSolicitor.Locations
|
||||
.FirstOrDefault(location => office.Address == location.Address);
|
||||
|
||||
if (location is null)
|
||||
AddNewLocationToSolicitor(office, matchingSolicitor);
|
||||
else
|
||||
UpdateExistingLocation(office, location);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExistingLocation(Location location, SolicitorLocation existingLocation)
|
||||
{
|
||||
existingLocation.Phone = location.Phone;
|
||||
existingLocation.LastModified = DateTime.UtcNow;
|
||||
|
||||
var lostRatings = existingLocation.LocationRatings
|
||||
.Where(locationRating => location.LocationRatings.All(rating => locationRating.Provider != rating.RatingProvider))
|
||||
.ToArray();
|
||||
|
||||
foreach (var rating in lostRatings)
|
||||
{
|
||||
existingLocation.LocationRatings.Remove(rating);
|
||||
SolicitorLocationRatings.Remove(rating);
|
||||
}
|
||||
|
||||
foreach (var rating in location.LocationRatings)
|
||||
{
|
||||
var existingRating = existingLocation.LocationRatings
|
||||
.FirstOrDefault(locationRating => locationRating.Provider == rating.RatingProvider);
|
||||
|
||||
if (existingRating is null)
|
||||
AddNewRatingToLocation(rating, existingLocation);
|
||||
else
|
||||
UpdateExistingLocationRating(rating, existingRating);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExistingLocationRating(Rating rating, SolicitorLocationRating existingRating)
|
||||
{
|
||||
existingRating.Value = rating.Value;
|
||||
existingRating.Maximum = rating.MaxValue;
|
||||
existingRating.LastModified = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void UpdateExistingCity(City city, Solicitor solicitor)
|
||||
{
|
||||
city.Solicitors.Add(solicitor);
|
||||
solicitor.Cities.Add(city);
|
||||
city.LastModified = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private Task<City?> GetCityByNameAsync(string cityName, CancellationToken cancellationToken)
|
||||
{
|
||||
return Cities
|
||||
.Include(city => city.Solicitors)
|
||||
.FirstOrDefaultAsync(
|
||||
city => city.Name == cityName,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Solicitors.HtmlParsing;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public static class DIExtensions
|
||||
{
|
||||
public static IServiceCollection AddHtmlParsing(this IServiceCollection services)
|
||||
|
||||
@ -33,7 +33,7 @@ internal class HtmlParser : IHtmlParser
|
||||
return ParseHtml(tokenEnumerator);
|
||||
}
|
||||
|
||||
private IEnumerable<string> LexHtml(IEnumerator<char> content)
|
||||
private IEnumerable<string> LexHtml(CharEnumerator content)
|
||||
{
|
||||
var work = "";
|
||||
while (content.MoveNext())
|
||||
@ -85,14 +85,14 @@ internal class HtmlParser : IHtmlParser
|
||||
}
|
||||
}
|
||||
|
||||
bool IsVoidOrSelfClosingElement(string token)
|
||||
private bool IsVoidOrSelfClosingElement(string token)
|
||||
{
|
||||
if (token.StartsWith("<!") || token.EndsWith("/>"))
|
||||
return true;
|
||||
|
||||
var tagName = token.Split(' ').First().Substring(1);
|
||||
var tagName = token.Split(' ').First()[1..];
|
||||
if (tagName.EndsWith('>'))
|
||||
tagName = tagName.Substring(0, tagName.Length - 1);
|
||||
tagName = tagName[..^1];
|
||||
return _voidElements.Contains(tagName);
|
||||
}
|
||||
|
||||
@ -100,8 +100,8 @@ internal class HtmlParser : IHtmlParser
|
||||
{
|
||||
var attributesString = string.Join(' ', tokenNoBraces.Split(' ').Skip(1));
|
||||
var attString = "";
|
||||
bool inQuote = false;
|
||||
foreach (char c in attributesString)
|
||||
var inQuote = false;
|
||||
foreach (var c in attributesString)
|
||||
{
|
||||
if (!inQuote && c == ' ')
|
||||
{
|
||||
|
||||
@ -2,19 +2,16 @@ using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Solicitors.HtmlParsing.Models;
|
||||
|
||||
internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
||||
internal class HtmlNode(string name, IHtmlAttribute[] attributes)
|
||||
: IHtmlNode
|
||||
{
|
||||
private readonly IHtmlAttribute[] _attributes = attributes;
|
||||
private readonly string _tagName = tagName;
|
||||
|
||||
public virtual bool TryGetByTagName(
|
||||
string tagName,
|
||||
[NotNullWhen(true)] out IHtmlNode? node,
|
||||
bool noChildren = false)
|
||||
{
|
||||
node = null;
|
||||
if (tagName == _tagName)
|
||||
if (tagName == name)
|
||||
node = this;
|
||||
|
||||
return node is not null;
|
||||
@ -24,7 +21,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
||||
{
|
||||
get
|
||||
{
|
||||
var classAttribute = _attributes
|
||||
var classAttribute = attributes
|
||||
.Where(a => a is HtmlValueAttribute)
|
||||
.Select(a => (a as HtmlValueAttribute)!)
|
||||
.FirstOrDefault(a => a.Name.Equals("class", StringComparison.CurrentCultureIgnoreCase));
|
||||
@ -55,7 +52,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
||||
bool noChildren = false)
|
||||
{
|
||||
node = null;
|
||||
if (tagName == _tagName && Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
|
||||
if (tagName == name && Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
|
||||
node = this;
|
||||
|
||||
return node is not null;
|
||||
@ -75,7 +72,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
||||
|
||||
public bool HasAttribute(string attributeName)
|
||||
{
|
||||
return _attributes
|
||||
return attributes
|
||||
.Any(a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
@ -83,7 +80,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
||||
{
|
||||
attributeValue = null;
|
||||
|
||||
var matchingAttribute = _attributes.FirstOrDefault(
|
||||
var matchingAttribute = attributes.FirstOrDefault(
|
||||
a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase)
|
||||
);
|
||||
|
||||
|
||||
@ -5,8 +5,6 @@ namespace Solicitors.HtmlParsing.Models;
|
||||
internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtmlNode[] children)
|
||||
: HtmlNode(tagName, attributes)
|
||||
{
|
||||
private readonly IHtmlNode[] _children = children;
|
||||
|
||||
public override bool TryGetByTagName(
|
||||
string tagName,
|
||||
[NotNullWhen(true)] out IHtmlNode? node,
|
||||
@ -15,7 +13,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
||||
node = null;
|
||||
if (!base.TryGetByTagName(tagName, out node, noChildren) && !noChildren)
|
||||
{
|
||||
var nodesToCheck = new Queue<IHtmlNode>(_children);
|
||||
var nodesToCheck = new Queue<IHtmlNode>(children);
|
||||
while (nodesToCheck.TryDequeue(out var child))
|
||||
{
|
||||
if (child.TryGetByTagName(tagName, out var childNode, true))
|
||||
@ -41,7 +39,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
||||
node = null;
|
||||
if (!base.TryGetByClass(className, out node, noChildren) && !noChildren)
|
||||
{
|
||||
var nodesToCheck = new Queue<IHtmlNode>(_children);
|
||||
var nodesToCheck = new Queue<IHtmlNode>(children);
|
||||
while (nodesToCheck.TryDequeue(out var child))
|
||||
{
|
||||
if (child.TryGetByClass(className, out var childNode, true))
|
||||
@ -68,7 +66,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
||||
node = null;
|
||||
if (!base.TryGetByTagNameAndClass(tagName, className, out node, noChildren) && !noChildren)
|
||||
{
|
||||
var nodesToCheck = new Queue<IHtmlNode>(_children);
|
||||
var nodesToCheck = new Queue<IHtmlNode>(children);
|
||||
while (nodesToCheck.TryDequeue(out var child))
|
||||
{
|
||||
if (child.TryGetByTagNameAndClass(tagName, className, out var childNode, true))
|
||||
@ -86,9 +84,9 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
||||
return node is not null;
|
||||
}
|
||||
|
||||
public override bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children)
|
||||
public override bool TryGetChildren([NotNullWhen(true)] out IEnumerable<IHtmlNode>? children1)
|
||||
{
|
||||
children = _children;
|
||||
return _children.Length != 0;
|
||||
children1 = children;
|
||||
return children.Length != 0;
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,6 @@ namespace Solicitors.HtmlParsing.Models;
|
||||
internal class StringNode(string content)
|
||||
: IHtmlNode
|
||||
{
|
||||
private readonly string _content = content;
|
||||
|
||||
public bool TryGetByTagName(
|
||||
string tagName,
|
||||
[NotNullWhen(true)] out IHtmlNode? node,
|
||||
@ -37,7 +35,7 @@ internal class StringNode(string content)
|
||||
|
||||
public bool TryGetText([NotNullWhen(true)] out string? text)
|
||||
{
|
||||
text = _content;
|
||||
text = content;
|
||||
return !string.IsNullOrEmpty(text);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,6 @@ import { SolicitorInfo } from "../models/solicitorSummary";
|
||||
import api from "./axios";
|
||||
|
||||
export default async function GetSolicitor(id : string) : Promise<SolicitorInfo | undefined> {
|
||||
const response = await api.get(`/conveyancors/${id}`);
|
||||
const response = await api.get(`/solicitors/${id}`);
|
||||
return response.data as SolicitorInfo;
|
||||
}
|
||||
@ -17,6 +17,6 @@ export default async function GetSolicitors(filters : FilterState) {
|
||||
if (filters.sortBy != null) {
|
||||
queryString += `&orderingType=${filters.sortBy}`;
|
||||
}
|
||||
const response = await api.get(`/conveyancors?${queryString}`);
|
||||
const response = await api.get(`/solicitors?${queryString}`);
|
||||
return response.data as PaginationResponse<SolicitorSummary>;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user