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
|
VOLUME /data
|
||||||
WORKDIR /App
|
WORKDIR /App
|
||||||
COPY --from=build /App/out .
|
COPY --from=build /App/out .
|
||||||
ENTRYPOINT ["dotnet", "SolicitorsApi.dll"]
|
ENTRYPOINT ["dotnet", "Solicitors.Api.dll"]
|
||||||
@ -1,4 +1,7 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<Folder Name="/app/">
|
||||||
|
<Project Path="app/Solicitors.Api/Solicitors.Api.csproj" />
|
||||||
|
</Folder>
|
||||||
<Folder Name="/libs/">
|
<Folder Name="/libs/">
|
||||||
<Project Path="libs/Solicitors.CacheBuild/Solicitors.CacheBuild.csproj" />
|
<Project Path="libs/Solicitors.CacheBuild/Solicitors.CacheBuild.csproj" />
|
||||||
<Project Path="libs/Solicitors.Core/Solicitors.Core.csproj" />
|
<Project Path="libs/Solicitors.Core/Solicitors.Core.csproj" />
|
||||||
@ -8,6 +11,4 @@
|
|||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/Solicitors.HtmlParsing.Tests/Solicitors.HtmlParsing.Tests.csproj" />
|
<Project Path="tests/Solicitors.HtmlParsing.Tests/Solicitors.HtmlParsing.Tests.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="SolicitorsApi/SolicitorsApi.csproj" />
|
|
||||||
<Project Path="Tester/Tester.csproj" />
|
|
||||||
</Solution>
|
</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",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:5084",
|
"applicationUrl": "http://localhost:5254",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://localhost:7128;http://localhost:5084",
|
"applicationUrl": "https://localhost:7226;http://localhost:5254",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@ -15,10 +15,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\libs\Solicitors.CacheBuild\Solicitors.CacheBuild.csproj" />
|
<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.Data\Solicitors.Data.csproj" />
|
|
||||||
<ProjectReference Include="..\libs\Solicitors.HtmlParsing\Solicitors.HtmlParsing.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
||||||
|
|
||||||
|
###
|
||||||
@ -6,4 +6,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
namespace Solicitors.CacheBuild;
|
namespace Solicitors.CacheBuild;
|
||||||
|
|
||||||
@ -10,6 +12,43 @@ public static class DIExtensions
|
|||||||
{
|
{
|
||||||
return services
|
return services
|
||||||
.AddScoped<ISolicitorParser, SolicitorsDotCom.SolicitorParser>()
|
.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;
|
namespace Solicitors.CacheBuild;
|
||||||
|
|
||||||
internal class SolicitorImporter : ISolicitorImporter
|
internal class SolicitorImporter(
|
||||||
|
ISolicitorParser parser,
|
||||||
|
ISolicitorRepository repository) : ISolicitorImporter
|
||||||
{
|
{
|
||||||
private readonly ISolicitorParser _parser;
|
|
||||||
private readonly ISolicitorRepository _repository;
|
|
||||||
|
|
||||||
public SolicitorImporter(
|
|
||||||
ISolicitorParser parser,
|
|
||||||
ISolicitorRepository repository)
|
|
||||||
{
|
|
||||||
_parser = parser;
|
|
||||||
_repository = repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunFullImport(CancellationToken cancellationToken)
|
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);
|
await RunImport(solicitor, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RunImport(SolicitorData solicitor, CancellationToken cancellationToken)
|
private Task RunImport(SolicitorData solicitor, CancellationToken cancellationToken)
|
||||||
=> _repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
|
=> repository.AddOrUpdateSolicitorAsync(solicitor, cancellationToken);
|
||||||
}
|
}
|
||||||
@ -13,6 +13,15 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using Solicitors.Core.Misc;
|
using Solicitors.Core.Misc;
|
||||||
using Solicitors.Core.Models.Imports;
|
using Solicitors.Core.Models.Imports;
|
||||||
using Solicitors.HtmlParsing;
|
using Solicitors.HtmlParsing;
|
||||||
@ -154,7 +153,7 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
ParseSidebar(sidebarChildren, out phone, out email, out website, out ratings);
|
ParseSidebar(sidebarChildren, out phone, out email, out website, out ratings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SolicitorData()
|
return new SolicitorData
|
||||||
{
|
{
|
||||||
Name = builder.Name,
|
Name = builder.Name,
|
||||||
UrlPath = builder.Path,
|
UrlPath = builder.Path,
|
||||||
@ -168,14 +167,14 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
|
private Location[] ParseOffices(IEnumerable<IHtmlNode> officeNodes)
|
||||||
=> officeNodes
|
=> officeNodes
|
||||||
.Select(ParseOffice)
|
.Select(ParseOffice)
|
||||||
.Where(x => x is not null)
|
.Where(x => x is not null)
|
||||||
.Cast<Location>()
|
.Cast<Location>()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
Location? ParseOffice(IHtmlNode officeNode)
|
private Location? ParseOffice(IHtmlNode officeNode)
|
||||||
{
|
{
|
||||||
if (!officeNode.TryGetChildren(out var officeChildren)
|
if (!officeNode.TryGetChildren(out var officeChildren)
|
||||||
|| !officeNode.TryGetByTagName("address", out var addressNode)
|
|| !officeNode.TryGetByTagName("address", out var addressNode)
|
||||||
@ -183,7 +182,7 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
Location? location = null;
|
Location? location = null;
|
||||||
string? address = "";
|
var address = "";
|
||||||
foreach (var line in addressLines)
|
foreach (var line in addressLines)
|
||||||
{
|
{
|
||||||
if (line.TryGetText(out var lineText))
|
if (line.TryGetText(out var lineText))
|
||||||
@ -214,7 +213,7 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ParseSidebar(
|
private void ParseSidebar(
|
||||||
IEnumerable<IHtmlNode> sidebarChildren,
|
IEnumerable<IHtmlNode> sidebarChildren,
|
||||||
out string? phone,
|
out string? phone,
|
||||||
out string? email,
|
out string? email,
|
||||||
@ -253,7 +252,7 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
foreach (var child in childrenArray.Where(x => x.TryGetByClass("rev-box", out _, true)))
|
foreach (var child in childrenArray.Where(x => x.TryGetByClass("rev-box", out _, true)))
|
||||||
{
|
{
|
||||||
if (child.TryGetAttributeValue("title", out var ratingString)
|
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)
|
&& child.TryGetByTagNameAndClass("img", "rev-logo", out var imgNode)
|
||||||
&& imgNode.TryGetAttributeValue("src", out var imgSrc)
|
&& imgNode.TryGetAttributeValue("src", out var imgSrc)
|
||||||
&& TryParseRatingSrc(imgSrc, out var ratingProvider))
|
&& 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);
|
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 = "";
|
provider = "";
|
||||||
imgSrc = imgSrc.Trim('"');
|
imgSrc = imgSrc.Trim('"');
|
||||||
var prefix = "/images/logo-";
|
const string prefix = "/images/logo-";
|
||||||
if (!imgSrc.StartsWith(prefix))
|
if (!imgSrc.StartsWith(prefix))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var providerAndExtension = imgSrc.Substring(prefix.Length);
|
var providerAndExtension = imgSrc[prefix.Length..];
|
||||||
provider = providerAndExtension.Split('.').First();
|
provider = providerAndExtension.Split('.').First();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -306,11 +305,11 @@ internal class SolicitorParser : ISolicitorParser
|
|||||||
bool TryParseOwnRatingString(string ratingString, out decimal rating)
|
bool TryParseOwnRatingString(string ratingString, out decimal rating)
|
||||||
{
|
{
|
||||||
rating = 0;
|
rating = 0;
|
||||||
var prefix = "Average review score : ";
|
const string prefix = "Average review score : ";
|
||||||
if (!ratingString.StartsWith(prefix))
|
if (!ratingString.StartsWith(prefix))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var ratingPart = ratingString.Substring(prefix.Length);
|
var ratingPart = ratingString[prefix.Length..];
|
||||||
return decimal.TryParse(ratingPart, out rating);
|
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 System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Solicitors.Core.Ordering;
|
||||||
|
|
||||||
namespace Solicitors.Core;
|
namespace Solicitors.Core;
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ public static class DIExtensions
|
|||||||
.AddScoped<ISolicitorService, SolicitorService>()
|
.AddScoped<ISolicitorService, SolicitorService>()
|
||||||
.AddScoped<IReadOnlySolicitorService>(sp => sp.GetRequiredService<ISolicitorService>())
|
.AddScoped<IReadOnlySolicitorService>(sp => sp.GetRequiredService<ISolicitorService>())
|
||||||
.AddScoped<IReadOnlyCitiesService, ReadOnlyCitiesService>()
|
.AddScoped<IReadOnlyCitiesService, ReadOnlyCitiesService>()
|
||||||
.AddScoped<IRatingsProviderService, RatingsProviderService>();
|
.AddScoped<IRatingsProviderService, RatingsProviderService>()
|
||||||
|
.AddScoped<ISolicitorComparerFactory, SolicitorComparerFactory>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,5 +10,5 @@ public interface IReadOnlySolicitorRepository
|
|||||||
|
|
||||||
public interface IReadOnlyCitiesRepository
|
public interface IReadOnlyCitiesRepository
|
||||||
{
|
{
|
||||||
IAsyncEnumerable<string> GetCitiesAsync(CancellationToken cancellationToken);
|
IAsyncEnumerable<string> GetCitiesAsync();
|
||||||
}
|
}
|
||||||
@ -4,5 +4,7 @@ namespace Solicitors.Core.Data;
|
|||||||
|
|
||||||
public interface ISolicitorRepository : IReadOnlySolicitorRepository
|
public interface ISolicitorRepository : IReadOnlySolicitorRepository
|
||||||
{
|
{
|
||||||
|
Task EnsureCreatedAsync(CancellationToken cancellationToken);
|
||||||
Task AddOrUpdateSolicitorAsync(SolicitorData solicitor, 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.Misc;
|
||||||
using Solicitors.Core.Models;
|
using Solicitors.Core.Models;
|
||||||
|
using Solicitors.Core.Models.View;
|
||||||
|
|
||||||
namespace Solicitors.Core;
|
namespace Solicitors.Core;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
namespace Solicitors.Core;
|
namespace Solicitors.Core;
|
||||||
|
|
||||||
public interface ISolicitorService : IReadOnlySolicitorService
|
public interface ISolicitorService : IReadOnlySolicitorService;
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,29 +1,19 @@
|
|||||||
namespace Solicitors.Core.Misc;
|
namespace Solicitors.Core.Misc;
|
||||||
|
|
||||||
public interface IFilter<T>
|
public interface IFilter<in T>
|
||||||
{
|
{
|
||||||
bool Filter(T item);
|
bool Filter(T item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WrapperFilter<T> : IFilter<T>
|
public class WrapperFilter<T>(
|
||||||
|
IFilter<T>? inner,
|
||||||
|
Func<T, bool> filter) : IFilter<T>
|
||||||
{
|
{
|
||||||
private readonly Func<T, bool> _filter;
|
|
||||||
private readonly IFilter<T>? _inner;
|
|
||||||
|
|
||||||
public WrapperFilter(
|
|
||||||
IFilter<T>? inner,
|
|
||||||
Func<T, bool> filter)
|
|
||||||
{
|
|
||||||
_inner = inner;
|
|
||||||
_filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public bool Filter(T item)
|
public bool Filter(T item)
|
||||||
{
|
{
|
||||||
if (_inner is not null)
|
if (inner is not null)
|
||||||
return _inner.Filter(item) && _filter(item);
|
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 Guid CityId { get; set; }
|
||||||
public required string Name { 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;
|
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 Address { get; } = Address;
|
||||||
public string Phone { get; } = phone;
|
public string Phone { get; } = Phone;
|
||||||
public Rating[] LocationRatings { get; } = locationRatings;
|
public Rating[] LocationRatings { get; } = LocationRatings;
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
namespace Solicitors.Core.Models.Imports;
|
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 Value { get; } = Value;
|
||||||
public decimal MaxValue { get; } = maxValue;
|
public decimal MaxValue { get; } = MaxValue;
|
||||||
public string RatingProvider { get; } = provider;
|
public string RatingProvider { get; } = RatingProvider;
|
||||||
public string RatingProviderImgSrc { get; } = imgSrc;
|
public string RatingProviderImgSrc { get; } = RatingProviderImgSrc;
|
||||||
}
|
}
|
||||||
@ -11,8 +11,9 @@ public record Solicitor
|
|||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Website { get; set; }
|
public string? Website { get; set; }
|
||||||
public string? ShortDescription { get; set; }
|
public string? ShortDescription { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
public List<City> Cities { get; } = new();
|
public List<City> Cities { get; } = [];
|
||||||
public List<SolicitorRating> Ratings { get; } = new();
|
public List<SolicitorRating> Ratings { get; } = [];
|
||||||
public List<SolicitorLocation> Locations { get; } = new();
|
public List<SolicitorLocation> Locations { get; } = [];
|
||||||
}
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
using Solicitors.Core.Models.View;
|
||||||
|
|
||||||
namespace Solicitors.Core.Models;
|
namespace Solicitors.Core.Models;
|
||||||
|
|
||||||
public record SolicitorInfo
|
public record SolicitorInfo
|
||||||
@ -25,7 +27,7 @@ public record SolicitorInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string ShortDescription { get; set; }
|
public string? ShortDescription { get; set; }
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
public string? Phone { get; set; }
|
public string? Phone { get; set; }
|
||||||
|
|||||||
@ -6,8 +6,9 @@ public record SolicitorLocation
|
|||||||
|
|
||||||
public required string Address { get; set; }
|
public required string Address { get; set; }
|
||||||
public required string Phone { 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 Guid SolicitorId { get; set; }
|
||||||
public required Solicitor Solicitor { 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 Value { get; set; }
|
||||||
public required decimal Maximum { get; set; }
|
public required decimal Maximum { get; set; }
|
||||||
public required string Provider { get; set; }
|
public required string Provider { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
public Guid SolicitorLocationId { get; set; }
|
public Guid SolicitorLocationId { get; set; }
|
||||||
public required SolicitorLocation SolicitorLocation { 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 Value { get; set; }
|
||||||
public required decimal Maximum { get; set; }
|
public required decimal Maximum { get; set; }
|
||||||
public required string Provider { get; set; }
|
public required string Provider { get; set; }
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
public Guid SolicitorId { get; set; }
|
public Guid SolicitorId { get; set; }
|
||||||
public required Solicitor Solicitor { get; set; }
|
public required Solicitor Solicitor { get; set; }
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Solicitors.Core.Models;
|
namespace Solicitors.Core.Models.View;
|
||||||
|
|
||||||
public record LocationInfo
|
public record LocationInfo
|
||||||
{
|
{
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Solicitors.Core.Models;
|
namespace Solicitors.Core.Models.View;
|
||||||
|
|
||||||
public record RatingInfo
|
public record RatingInfo
|
||||||
{
|
{
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Solicitors.Core.Models;
|
namespace Solicitors.Core.Models.View;
|
||||||
|
|
||||||
public record SolicitorSummary
|
public record SolicitorSummary
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using Solicitors.Core.Models;
|
using Solicitors.Core.Models;
|
||||||
|
|
||||||
namespace Solicitors.Core.Misc;
|
namespace Solicitors.Core.Ordering;
|
||||||
|
|
||||||
internal class DefaultSolicitorDataOrdering : IComparer<Solicitor>
|
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;
|
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)
|
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;
|
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)
|
public async Task<string[]> GetAllCitiesAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cities = _repo.GetCitiesAsync(cancellationToken);
|
var cities = repo.GetCitiesAsync();
|
||||||
return await cities.ToArrayAsync(cancellationToken);
|
return await cities.ToArrayAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,18 +1,13 @@
|
|||||||
using Solicitors.Core.Data;
|
using Solicitors.Core.Data;
|
||||||
using Solicitors.Core.Misc;
|
using Solicitors.Core.Misc;
|
||||||
using Solicitors.Core.Models;
|
using Solicitors.Core.Models;
|
||||||
|
using Solicitors.Core.Models.View;
|
||||||
|
using Solicitors.Core.Ordering;
|
||||||
|
|
||||||
namespace Solicitors.Core;
|
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(
|
public async Task<PaginationResponse<SolicitorSummary>> GetSolicitorSummariesAsync(
|
||||||
Pagination pagination,
|
Pagination pagination,
|
||||||
string ratingsProvider,
|
string ratingsProvider,
|
||||||
@ -20,14 +15,13 @@ internal class SolicitorService : ISolicitorService
|
|||||||
IComparer<Solicitor>? ordering = null,
|
IComparer<Solicitor>? ordering = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var allSolicitors = _repo.GetAllSolicitorsAsync();
|
var allSolicitors = repo.GetAllSolicitorsAsync();
|
||||||
if (filter is not null)
|
if (filter is not null)
|
||||||
allSolicitors = allSolicitors.Where(filter.Filter);
|
allSolicitors = allSolicitors.Where(filter.Filter);
|
||||||
|
|
||||||
var matchingSolicitors = await allSolicitors.ToArrayAsync(cancellationToken);
|
var matchingSolicitors = await allSolicitors.ToArrayAsync(cancellationToken);
|
||||||
|
|
||||||
if (ordering is null)
|
ordering ??= DefaultSolicitorDataOrdering.Instance;
|
||||||
ordering = DefaultSolicitorDataOrdering.Instance;
|
|
||||||
|
|
||||||
var results = matchingSolicitors
|
var results = matchingSolicitors
|
||||||
.OrderBy(x => x, ordering)
|
.OrderBy(x => x, ordering)
|
||||||
@ -41,7 +35,7 @@ internal class SolicitorService : ISolicitorService
|
|||||||
|
|
||||||
public async Task<SolicitorInfo?> GetSolicitorInfoByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
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)
|
if (data is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Solicitors.Core;
|
|
||||||
using Solicitors.Core.Data;
|
using Solicitors.Core.Data;
|
||||||
using Solicitors.Data.RepositorySetup;
|
using Solicitors.Data.RepositorySetup;
|
||||||
using Solicitors.Data.RepositorySetup.InMemory;
|
using Solicitors.Data.RepositorySetup.InMemory;
|
||||||
@ -12,7 +11,7 @@ namespace Solicitors.Data;
|
|||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
public static class DIExtensions
|
public static class DIExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddData(this IServiceCollection services, IRepoSetupOptions options)
|
public static IServiceCollection AddData(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
return services
|
return services
|
||||||
.AddScoped<SolicitorsRepository>()
|
.AddScoped<SolicitorsRepository>()
|
||||||
@ -20,17 +19,30 @@ public static class DIExtensions
|
|||||||
.AddScoped<IReadOnlySolicitorRepository>(sp => sp.GetRequiredService<ISolicitorRepository>())
|
.AddScoped<IReadOnlySolicitorRepository>(sp => sp.GetRequiredService<ISolicitorRepository>())
|
||||||
.AddScoped<IReadOnlyCitiesRepository>(sp => sp.GetRequiredService<SolicitorsRepository>())
|
.AddScoped<IReadOnlyCitiesRepository>(sp => sp.GetRequiredService<SolicitorsRepository>())
|
||||||
.AddScoped<IRatingsProviderRepository>(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)
|
var options = ReadConfigForOptions(configuration);
|
||||||
return new InMemorySetupService(inMemOptions);
|
|
||||||
|
|
||||||
if (options is SqliteDbSetupOptions sqliteOptions)
|
return options switch
|
||||||
return new SqliteSetupService(sqliteOptions);
|
{
|
||||||
|
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;
|
namespace Solicitors.Data.RepositorySetup;
|
||||||
|
|
||||||
public interface IRepoSetupOptions
|
public interface IRepoSetupOptions;
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
namespace Solicitors.Data.RepositorySetup.Sqlite;
|
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)
|
public void OnConfiguring(DbContextOptionsBuilder options)
|
||||||
{
|
{
|
||||||
var folder = Environment.SpecialFolder.MyDocuments;
|
options.UseSqlite($"Data Source={config.DbPath}");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" 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.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" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Solicitors.Core;
|
|
||||||
using Solicitors.Core.Data;
|
using Solicitors.Core.Data;
|
||||||
using Solicitors.Core.Models;
|
using Solicitors.Core.Models;
|
||||||
using Solicitors.Core.Models.Imports;
|
using Solicitors.Core.Models.Imports;
|
||||||
@ -7,15 +6,9 @@ using Solicitors.Data.RepositorySetup;
|
|||||||
|
|
||||||
namespace Solicitors.Data;
|
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<Solicitor> Solicitors { get; set; }
|
||||||
public DbSet<SolicitorRating> SolicitorRatings { get; set; }
|
public DbSet<SolicitorRating> SolicitorRatings { get; set; }
|
||||||
public DbSet<SolicitorLocation> SolicitorLocations { get; set; }
|
public DbSet<SolicitorLocation> SolicitorLocations { get; set; }
|
||||||
@ -23,7 +16,10 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
|||||||
public DbSet<City> Cities { get; set; }
|
public DbSet<City> Cities { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> _setup.OnConfiguring(optionsBuilder);
|
=> setup.OnConfiguring(optionsBuilder);
|
||||||
|
|
||||||
|
public Task EnsureCreatedAsync(CancellationToken cancellationToken)
|
||||||
|
=> Database.EnsureCreatedAsync(cancellationToken);
|
||||||
|
|
||||||
public IAsyncEnumerable<Solicitor> GetAllSolicitorsAsync()
|
public IAsyncEnumerable<Solicitor> GetAllSolicitorsAsync()
|
||||||
=> Solicitors
|
=> Solicitors
|
||||||
@ -41,104 +37,7 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
|||||||
.Include(solicitor => solicitor.Ratings)
|
.Include(solicitor => solicitor.Ratings)
|
||||||
.FirstOrDefaultAsync(x => x.SolicitorId == id, cancellationToken);
|
.FirstOrDefaultAsync(x => x.SolicitorId == id, cancellationToken);
|
||||||
|
|
||||||
public async Task AddOrUpdateSolicitorAsync(SolicitorData solicitor, CancellationToken cancellationToken)
|
public IAsyncEnumerable<string> GetCitiesAsync()
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
return Cities
|
return Cities
|
||||||
.Select(city => city.Name)
|
.Select(city => city.Name)
|
||||||
@ -154,4 +53,286 @@ internal class SolicitorsRepository : DbContext, ISolicitorRepository, IReadOnly
|
|||||||
.ThenBy(x => x)
|
.ThenBy(x => x)
|
||||||
.AsAsyncEnumerable();
|
.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;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Solicitors.HtmlParsing;
|
namespace Solicitors.HtmlParsing;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||||
public static class DIExtensions
|
public static class DIExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddHtmlParsing(this IServiceCollection services)
|
public static IServiceCollection AddHtmlParsing(this IServiceCollection services)
|
||||||
|
|||||||
@ -33,7 +33,7 @@ internal class HtmlParser : IHtmlParser
|
|||||||
return ParseHtml(tokenEnumerator);
|
return ParseHtml(tokenEnumerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<string> LexHtml(IEnumerator<char> content)
|
private IEnumerable<string> LexHtml(CharEnumerator content)
|
||||||
{
|
{
|
||||||
var work = "";
|
var work = "";
|
||||||
while (content.MoveNext())
|
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("/>"))
|
if (token.StartsWith("<!") || token.EndsWith("/>"))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var tagName = token.Split(' ').First().Substring(1);
|
var tagName = token.Split(' ').First()[1..];
|
||||||
if (tagName.EndsWith('>'))
|
if (tagName.EndsWith('>'))
|
||||||
tagName = tagName.Substring(0, tagName.Length - 1);
|
tagName = tagName[..^1];
|
||||||
return _voidElements.Contains(tagName);
|
return _voidElements.Contains(tagName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +100,8 @@ internal class HtmlParser : IHtmlParser
|
|||||||
{
|
{
|
||||||
var attributesString = string.Join(' ', tokenNoBraces.Split(' ').Skip(1));
|
var attributesString = string.Join(' ', tokenNoBraces.Split(' ').Skip(1));
|
||||||
var attString = "";
|
var attString = "";
|
||||||
bool inQuote = false;
|
var inQuote = false;
|
||||||
foreach (char c in attributesString)
|
foreach (var c in attributesString)
|
||||||
{
|
{
|
||||||
if (!inQuote && c == ' ')
|
if (!inQuote && c == ' ')
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,19 +2,16 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
|
|
||||||
namespace Solicitors.HtmlParsing.Models;
|
namespace Solicitors.HtmlParsing.Models;
|
||||||
|
|
||||||
internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
internal class HtmlNode(string name, IHtmlAttribute[] attributes)
|
||||||
: IHtmlNode
|
: IHtmlNode
|
||||||
{
|
{
|
||||||
private readonly IHtmlAttribute[] _attributes = attributes;
|
|
||||||
private readonly string _tagName = tagName;
|
|
||||||
|
|
||||||
public virtual bool TryGetByTagName(
|
public virtual bool TryGetByTagName(
|
||||||
string tagName,
|
string tagName,
|
||||||
[NotNullWhen(true)] out IHtmlNode? node,
|
[NotNullWhen(true)] out IHtmlNode? node,
|
||||||
bool noChildren = false)
|
bool noChildren = false)
|
||||||
{
|
{
|
||||||
node = null;
|
node = null;
|
||||||
if (tagName == _tagName)
|
if (tagName == name)
|
||||||
node = this;
|
node = this;
|
||||||
|
|
||||||
return node is not null;
|
return node is not null;
|
||||||
@ -24,7 +21,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var classAttribute = _attributes
|
var classAttribute = attributes
|
||||||
.Where(a => a is HtmlValueAttribute)
|
.Where(a => a is HtmlValueAttribute)
|
||||||
.Select(a => (a as HtmlValueAttribute)!)
|
.Select(a => (a as HtmlValueAttribute)!)
|
||||||
.FirstOrDefault(a => a.Name.Equals("class", StringComparison.CurrentCultureIgnoreCase));
|
.FirstOrDefault(a => a.Name.Equals("class", StringComparison.CurrentCultureIgnoreCase));
|
||||||
@ -55,7 +52,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
|||||||
bool noChildren = false)
|
bool noChildren = false)
|
||||||
{
|
{
|
||||||
node = null;
|
node = null;
|
||||||
if (tagName == _tagName && Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
|
if (tagName == name && Classes.Contains(className, StringComparer.InvariantCultureIgnoreCase))
|
||||||
node = this;
|
node = this;
|
||||||
|
|
||||||
return node is not null;
|
return node is not null;
|
||||||
@ -75,7 +72,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
|||||||
|
|
||||||
public bool HasAttribute(string attributeName)
|
public bool HasAttribute(string attributeName)
|
||||||
{
|
{
|
||||||
return _attributes
|
return attributes
|
||||||
.Any(a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase));
|
.Any(a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +80,7 @@ internal class HtmlNode(string tagName, IHtmlAttribute[] attributes)
|
|||||||
{
|
{
|
||||||
attributeValue = null;
|
attributeValue = null;
|
||||||
|
|
||||||
var matchingAttribute = _attributes.FirstOrDefault(
|
var matchingAttribute = attributes.FirstOrDefault(
|
||||||
a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase)
|
a => a.Name.Equals(attributeName, StringComparison.InvariantCultureIgnoreCase)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ namespace Solicitors.HtmlParsing.Models;
|
|||||||
internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtmlNode[] children)
|
internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtmlNode[] children)
|
||||||
: HtmlNode(tagName, attributes)
|
: HtmlNode(tagName, attributes)
|
||||||
{
|
{
|
||||||
private readonly IHtmlNode[] _children = children;
|
|
||||||
|
|
||||||
public override bool TryGetByTagName(
|
public override bool TryGetByTagName(
|
||||||
string tagName,
|
string tagName,
|
||||||
[NotNullWhen(true)] out IHtmlNode? node,
|
[NotNullWhen(true)] out IHtmlNode? node,
|
||||||
@ -15,7 +13,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
|||||||
node = null;
|
node = null;
|
||||||
if (!base.TryGetByTagName(tagName, out node, noChildren) && !noChildren)
|
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))
|
while (nodesToCheck.TryDequeue(out var child))
|
||||||
{
|
{
|
||||||
if (child.TryGetByTagName(tagName, out var childNode, true))
|
if (child.TryGetByTagName(tagName, out var childNode, true))
|
||||||
@ -41,7 +39,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
|||||||
node = null;
|
node = null;
|
||||||
if (!base.TryGetByClass(className, out node, noChildren) && !noChildren)
|
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))
|
while (nodesToCheck.TryDequeue(out var child))
|
||||||
{
|
{
|
||||||
if (child.TryGetByClass(className, out var childNode, true))
|
if (child.TryGetByClass(className, out var childNode, true))
|
||||||
@ -68,7 +66,7 @@ internal class ParentHtmlNode(string tagName, IHtmlAttribute[] attributes, IHtml
|
|||||||
node = null;
|
node = null;
|
||||||
if (!base.TryGetByTagNameAndClass(tagName, className, out node, noChildren) && !noChildren)
|
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))
|
while (nodesToCheck.TryDequeue(out var child))
|
||||||
{
|
{
|
||||||
if (child.TryGetByTagNameAndClass(tagName, className, out var childNode, true))
|
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;
|
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;
|
children1 = children;
|
||||||
return _children.Length != 0;
|
return children.Length != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,8 +5,6 @@ namespace Solicitors.HtmlParsing.Models;
|
|||||||
internal class StringNode(string content)
|
internal class StringNode(string content)
|
||||||
: IHtmlNode
|
: IHtmlNode
|
||||||
{
|
{
|
||||||
private readonly string _content = content;
|
|
||||||
|
|
||||||
public bool TryGetByTagName(
|
public bool TryGetByTagName(
|
||||||
string tagName,
|
string tagName,
|
||||||
[NotNullWhen(true)] out IHtmlNode? node,
|
[NotNullWhen(true)] out IHtmlNode? node,
|
||||||
@ -37,7 +35,7 @@ internal class StringNode(string content)
|
|||||||
|
|
||||||
public bool TryGetText([NotNullWhen(true)] out string? text)
|
public bool TryGetText([NotNullWhen(true)] out string? text)
|
||||||
{
|
{
|
||||||
text = _content;
|
text = content;
|
||||||
return !string.IsNullOrEmpty(text);
|
return !string.IsNullOrEmpty(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,6 @@ import { SolicitorInfo } from "../models/solicitorSummary";
|
|||||||
import api from "./axios";
|
import api from "./axios";
|
||||||
|
|
||||||
export default async function GetSolicitor(id : string) : Promise<SolicitorInfo | undefined> {
|
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;
|
return response.data as SolicitorInfo;
|
||||||
}
|
}
|
||||||
@ -17,6 +17,6 @@ export default async function GetSolicitors(filters : FilterState) {
|
|||||||
if (filters.sortBy != null) {
|
if (filters.sortBy != null) {
|
||||||
queryString += `&orderingType=${filters.sortBy}`;
|
queryString += `&orderingType=${filters.sortBy}`;
|
||||||
}
|
}
|
||||||
const response = await api.get(`/conveyancors?${queryString}`);
|
const response = await api.get(`/solicitors?${queryString}`);
|
||||||
return response.data as PaginationResponse<SolicitorSummary>;
|
return response.data as PaginationResponse<SolicitorSummary>;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user