refactor(svcs): ♻🔥 use file scoped namespaces

This commit is contained in:
Collin M. Barrett 2021-11-11 12:07:05 -06:00
parent 6c9dc865b5
commit 1649ac6778
80 changed files with 1777 additions and 1853 deletions

View file

@ -138,4 +138,7 @@ visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public
dotnet_diagnostic.CS1591.severity = silent
#https://github.com/JosefPihrt/Roslynator/blob/master/docs/Options.md
roslynator.RCS1090.invert = true
roslynator.RCS1090.invert = true
#https://github.com/SonarSource/sonar-dotnet/issues/4731
dotnet_diagnostic.S3903.severity = silent

View file

@ -1,29 +1,28 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace FilterLists.Archival.Api.Controllers
namespace FilterLists.Archival.Api.Controllers;
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
// TODO: de-duplicate into SharedKernel
public class ErrorController : ControllerBase
{
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
// TODO: de-duplicate into SharedKernel
public class ErrorController : ControllerBase
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment([FromServices] IWebHostEnvironment webHostEnvironment)
{
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment([FromServices] IWebHostEnvironment webHostEnvironment)
if (webHostEnvironment.EnvironmentName != "Development")
{
if (webHostEnvironment.EnvironmentName != "Development")
{
throw new InvalidOperationException("This shouldn't be invoked in non-development environments.");
}
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(context?.Error.StackTrace, title: context?.Error.Message);
throw new InvalidOperationException("This shouldn't be invoked in non-development environments.");
}
[Route("/error")]
public IActionResult Error()
{
return Problem();
}
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(context?.Error.StackTrace, title: context?.Error.Message);
}
[Route("/error")]
public IActionResult Error()
{
return Problem();
}
}

View file

@ -1,21 +1,20 @@
using Microsoft.AspNetCore.Mvc;
namespace FilterLists.Archival.Api.Controllers
namespace FilterLists.Archival.Api.Controllers;
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class PingController : ControllerBase
{
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class PingController : ControllerBase
/// <summary>
/// A sample endpoint.
/// </summary>
/// <returns>Pong.</returns>
[HttpGet]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public ActionResult<string> Ping()
{
/// <summary>
/// A sample endpoint.
/// </summary>
/// <returns>Pong.</returns>
[HttpGet]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public ActionResult<string> Ping()
{
return "pong";
}
return "pong";
}
}

View file

@ -1,22 +1,21 @@
using FilterLists.Archival.Application;
using FilterLists.SharedKernel.Logging;
namespace FilterLists.Archival.Api
{
public static class Program
{
// TODO: migrate to new hosting model https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60?view=aspnetcore-6.0&tabs=visual-studio#new-hosting-model
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.TryRunWithLoggingAsync();
}
namespace FilterLists.Archival.Api;
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseApplication()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
public static class Program
{
// TODO: migrate to new hosting model https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60?view=aspnetcore-6.0&tabs=visual-studio#new-hosting-model
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.TryRunWithLoggingAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseApplication()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
}

View file

@ -1,33 +1,32 @@
using System.Text.Json.Serialization;
using FilterLists.Archival.Application;
namespace FilterLists.Archival.Api
namespace FilterLists.Archival.Api;
internal class Startup
{
internal class Startup
public Startup(IConfiguration configuration)
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(o => o.LowercaseUrls = true);
services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull);
services.AddSwaggerGen();
services.AddApplicationServices(Configuration);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting(o => o.LowercaseUrls = true);
services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull);
services.AddSwaggerGen();
services.AddApplicationServices(Configuration);
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseApplication();
app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
app.UseSwagger();
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseApplication();
app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
app.UseSwagger();
}
}

View file

@ -1,45 +1,44 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
namespace FilterLists.Archival.Api
namespace FilterLists.Archival.Api;
internal static class SwaggerExtensions
{
internal static class SwaggerExtensions
public static void AddSwaggerGen(this IServiceCollection services)
{
public static void AddSwaggerGen(this IServiceCollection services)
services.AddSwaggerGen(o =>
{
services.AddSwaggerGen(o =>
o.SwaggerDoc("v1", new OpenApiInfo
{
o.SwaggerDoc("v1", new OpenApiInfo
Title = "FilterLists Archival API",
Description =
"An ASP.NET Core API archiving and serving copies of FilterLists for mirrors and analysis.",
Version = "v1",
//TermsOfService = "",
Contact = new OpenApiContact { Name = "FilterLists", Url = new Uri("https://filterlists.com") },
License = new OpenApiLicense
{
Title = "FilterLists Archival API",
Description =
"An ASP.NET Core API archiving and serving copies of FilterLists for mirrors and analysis.",
Version = "v1",
//TermsOfService = "",
Contact = new OpenApiContact { Name = "FilterLists", Url = new Uri("https://filterlists.com") },
License = new OpenApiLicense
{
Name = "MIT License",
Url = new Uri("https://github.com/collinbarrett/FilterLists/blob/master/LICENSE")
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
o.IncludeXmlComments(xmlPath);
Name = "MIT License",
Url = new Uri("https://github.com/collinbarrett/FilterLists/blob/master/LICENSE")
}
});
}
public static void UseSwagger(this IApplicationBuilder app)
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
o.IncludeXmlComments(xmlPath);
});
}
public static void UseSwagger(this IApplicationBuilder app)
{
app.UseSwagger(o =>
{
app.UseSwagger(o =>
o.RouteTemplate = "{documentName}/swagger.json";
o.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List<OpenApiServer>
{
o.RouteTemplate = "{documentName}/swagger.json";
o.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List<OpenApiServer>
{
new() { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/api/archival" }
});
new() { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/api/archival" }
});
}
});
}
}

View file

@ -6,93 +6,92 @@
using MediatR;
using Microsoft.Extensions.Logging;
namespace FilterLists.Archival.Application.Commands
{
public static class ArchiveList
{
public class Command : IRequest
{
public Command(int listId)
{
ListId = listId;
}
namespace FilterLists.Archival.Application.Commands;
public int ListId { get; }
public static class ArchiveList
{
public class Command : IRequest
{
public Command(int listId)
{
ListId = listId;
}
public class Handler : IRequestHandler<Command, Unit>
public int ListId { get; }
}
public class Handler : IRequestHandler<Command, Unit>
{
private readonly IHttpContentClient _client;
private readonly IDirectoryApi _directory;
private readonly ILogger _logger;
private readonly IListArchiveRepository _repo;
public Handler(
IHttpContentClient httpContentClient,
IDirectoryApi directoryApi,
ILogger<Handler> logger,
IListArchiveRepository listArchiveRepository)
{
private readonly IHttpContentClient _client;
private readonly IDirectoryApi _directory;
private readonly ILogger _logger;
private readonly IListArchiveRepository _repo;
_client = httpContentClient;
_directory = directoryApi;
_logger = logger;
_repo = listArchiveRepository;
}
public Handler(
IHttpContentClient httpContentClient,
IDirectoryApi directoryApi,
ILogger<Handler> logger,
IListArchiveRepository listArchiveRepository)
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
_logger.LogInformation("Archiving list {ListId}", request.ListId);
var segmentUrls = (await GetSegmentUrlsAsync(request.ListId, cancellationToken)).ToList();
if (segmentUrls.Count > 0)
{
_client = httpContentClient;
_directory = directoryApi;
_logger = logger;
_repo = listArchiveRepository;
var list = GetList(request.ListId, segmentUrls, cancellationToken);
await _repo.AddAsync(list, cancellationToken);
_repo.Commit();
_logger.LogInformation(
"Archived segment(s) {@SegmentNumbers} of list {ListId}",
segmentUrls.Select(s => s.SegmentNumber),
request.ListId);
}
else
{
_logger.LogWarning("List {ListId} has no URLs to archive", request.ListId);
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
_logger.LogInformation("Archiving list {ListId}", request.ListId);
return Unit.Value;
}
var segmentUrls = (await GetSegmentUrlsAsync(request.ListId, cancellationToken)).ToList();
if (segmentUrls.Count > 0)
private async Task<IEnumerable<ListDetailsVm.ViewUrlVm>> GetSegmentUrlsAsync(
int listId,
CancellationToken cancellationToken)
{
var listDetails = await _directory.GetListDetailsAsync(listId, cancellationToken);
return listDetails.ViewUrls?
.GroupBy(u => u.SegmentNumber, (_, ue) => ue.OrderBy(u => u.Primariness).First()) ??
new List<ListDetailsVm.ViewUrlVm>();
}
private ListArchive GetList(
int listId,
IEnumerable<ListDetailsVm.ViewUrlVm> segmentUrls,
CancellationToken cancellationToken)
{
var segmentsAsync = GetSegmentsAsync(segmentUrls, cancellationToken);
return new ListArchive(listId, segmentsAsync);
}
private async IAsyncEnumerable<ListArchiveSegment> GetSegmentsAsync(
IEnumerable<ListDetailsVm.ViewUrlVm> segmentUrls,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var segment in segmentUrls)
{
var content = await _client.GetContentAsync(segment.Url, cancellationToken);
if (content != Stream.Null)
{
var list = GetList(request.ListId, segmentUrls, cancellationToken);
await _repo.AddAsync(list, cancellationToken);
_repo.Commit();
_logger.LogInformation(
"Archived segment(s) {@SegmentNumbers} of list {ListId}",
segmentUrls.Select(s => s.SegmentNumber),
request.ListId);
}
else
{
_logger.LogWarning("List {ListId} has no URLs to archive", request.ListId);
}
return Unit.Value;
}
private async Task<IEnumerable<ListDetailsVm.ViewUrlVm>> GetSegmentUrlsAsync(
int listId,
CancellationToken cancellationToken)
{
var listDetails = await _directory.GetListDetailsAsync(listId, cancellationToken);
return listDetails.ViewUrls?
.GroupBy(u => u.SegmentNumber, (_, ue) => ue.OrderBy(u => u.Primariness).First()) ??
new List<ListDetailsVm.ViewUrlVm>();
}
private ListArchive GetList(
int listId,
IEnumerable<ListDetailsVm.ViewUrlVm> segmentUrls,
CancellationToken cancellationToken)
{
var segmentsAsync = GetSegmentsAsync(segmentUrls, cancellationToken);
return new ListArchive(listId, segmentsAsync);
}
private async IAsyncEnumerable<ListArchiveSegment> GetSegmentsAsync(
IEnumerable<ListDetailsVm.ViewUrlVm> segmentUrls,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var segment in segmentUrls)
{
var content = await _client.GetContentAsync(segment.Url, cancellationToken);
if (content != Stream.Null)
{
yield return new ListArchiveSegment(segment.Url, content);
}
yield return new ListArchiveSegment(segment.Url, content);
}
}
}

View file

@ -3,51 +3,50 @@
using MediatR;
using Microsoft.Extensions.Logging;
namespace FilterLists.Archival.Application.Commands
namespace FilterLists.Archival.Application.Commands;
public static class EnqueueArchiveAllLists
{
public static class EnqueueArchiveAllLists
public class Command : IRequest
{
public class Command : IRequest
}
public class Handler : IRequestHandler<Command, Unit>
{
private readonly IDirectoryApi _directory;
private readonly ILogger _logger;
public Handler(IDirectoryApi directory, ILogger<Handler> logger)
{
_directory = directory;
_logger = logger;
}
public class Handler : IRequestHandler<Command, Unit>
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
private readonly IDirectoryApi _directory;
private readonly ILogger _logger;
var r = new Random();
var lists = (await _directory.GetListsAsync(cancellationToken)).OrderBy(_ => r.Next()).ToList();
public Handler(IDirectoryApi directory, ILogger<Handler> logger)
{
_directory = directory;
_logger = logger;
}
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
var r = new Random();
var lists = (await _directory.GetListsAsync(cancellationToken)).OrderBy(_ => r.Next()).ToList();
int archiveCount;
TimeSpan spacing;
int archiveCount;
TimeSpan spacing;
#if DEBUG
archiveCount = 0;
spacing = TimeSpan.FromSeconds(5);
archiveCount = 0;
spacing = TimeSpan.FromSeconds(5);
#else
archiveCount = 0;
//archiveCount = lists.Count;
spacing = TimeSpan.FromSeconds((double)86400 / lists.Count);
#endif
_logger.LogInformation("Enqueuing archival of {ArchiveCount} lists spaced {Spacing} seconds apart.",
archiveCount, spacing.Seconds);
_logger.LogInformation("Enqueuing archival of {ArchiveCount} lists spaced {Spacing} seconds apart.",
archiveCount, spacing.Seconds);
for (var i = 0; i < archiveCount; i++)
{
new ArchiveList.Command(lists[i].Id).ScheduleBackgroundJob(i * spacing);
}
return Unit.Value;
for (var i = 0; i < archiveCount; i++)
{
new ArchiveList.Command(lists[i].Id).ScheduleBackgroundJob(i * spacing);
}
return Unit.Value;
}
}
}

View file

@ -8,35 +8,34 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Archival.Application
namespace FilterLists.Archival.Application;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static IHostBuilder UseApplication(this IHostBuilder hostBuilder)
{
public static IHostBuilder UseApplication(this IHostBuilder hostBuilder)
{
return hostBuilder.UseInfrastructure();
}
return hostBuilder.UseInfrastructure();
}
public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(ConfigurationExtensions).Assembly);
services.AddInfrastructureServices(configuration);
}
public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(ConfigurationExtensions).Assembly);
services.AddInfrastructureServices(configuration);
}
public static void UseApplication(this IApplicationBuilder app)
{
app.UseInfrastructure();
ScheduleArchival();
}
public static void UseApplication(this IApplicationBuilder app)
{
app.UseInfrastructure();
ScheduleArchival();
}
private static void ScheduleArchival()
{
private static void ScheduleArchival()
{
#if DEBUG
JobStorage.Current?.GetMonitoringApi()?.PurgeJobs();
new EnqueueArchiveAllLists.Command().EnqueueBackgroundJob();
JobStorage.Current?.GetMonitoringApi()?.PurgeJobs();
new EnqueueArchiveAllLists.Command().EnqueueBackgroundJob();
#else
new EnqueueArchiveAllLists.Command().AddOrUpdateRecurringJob(Cron.Daily);
#endif
}
}
}

View file

@ -1,17 +1,16 @@
namespace FilterLists.Archival.Domain.Extensions
namespace FilterLists.Archival.Domain.Extensions;
internal static class UriExtension
{
internal static class UriExtension
private static readonly Uri DummyBaseUri = new("http://localhost");
public static string GetFileExtension(this Uri uri)
{
private static readonly Uri DummyBaseUri = new("http://localhost");
public static string GetFileExtension(this Uri uri)
if (!uri.IsAbsoluteUri)
{
if (!uri.IsAbsoluteUri)
{
uri = new Uri(DummyBaseUri, uri);
}
return Path.GetExtension(uri.LocalPath);
uri = new Uri(DummyBaseUri, uri);
}
return Path.GetExtension(uri.LocalPath);
}
}

View file

@ -1,9 +1,8 @@
using FilterLists.Archival.Domain.SeedWork;
namespace FilterLists.Archival.Domain.ListArchives
namespace FilterLists.Archival.Domain.ListArchives;
public interface IListArchiveRepository : IUnitOfWork
{
public interface IListArchiveRepository : IUnitOfWork
{
Task AddAsync(ListArchive listArchive, CancellationToken cancellationToken);
}
Task AddAsync(ListArchive listArchive, CancellationToken cancellationToken);
}

View file

@ -1,15 +1,14 @@
namespace FilterLists.Archival.Domain.ListArchives
namespace FilterLists.Archival.Domain.ListArchives;
public class ListArchive
{
public class ListArchive
public ListArchive(int id, IAsyncEnumerable<ListArchiveSegment> segments)
{
public ListArchive(int id, IAsyncEnumerable<ListArchiveSegment> segments)
{
Id = id;
Segments = segments;
}
public int Id { get; }
public IAsyncEnumerable<ListArchiveSegment> Segments { get; }
Id = id;
Segments = segments;
}
public int Id { get; }
public IAsyncEnumerable<ListArchiveSegment> Segments { get; }
}

View file

@ -1,15 +1,14 @@
namespace FilterLists.Archival.Domain.ListArchives
namespace FilterLists.Archival.Domain.ListArchives;
public class ListArchiveSegment
{
public class ListArchiveSegment
public ListArchiveSegment(Uri sourceUri, Stream content)
{
public ListArchiveSegment(Uri sourceUri, Stream content)
{
Extension = ListFileExtension.FromUri(sourceUri);
Content = content;
}
public ListFileExtension Extension { get; }
public Stream Content { get; }
Extension = ListFileExtension.FromUri(sourceUri);
Content = content;
}
public ListFileExtension Extension { get; }
public Stream Content { get; }
}

View file

@ -1,64 +1,63 @@
using FilterLists.Archival.Domain.Extensions;
using FilterLists.Archival.Domain.SeedWork;
namespace FilterLists.Archival.Domain.ListArchives
namespace FilterLists.Archival.Domain.ListArchives;
public sealed class ListFileExtension : ValueObject
{
public sealed class ListFileExtension : ValueObject
private static readonly IDictionary<string, (bool IsPlainText, bool IsMeaningfulToConsumer)> Info =
new Dictionary<string, (bool, bool)>
{
{ string.Empty, (true, false) },
{ ".txt", (true, false) },
{ ".7z", (false, false) },
{ ".action", (true, true) }, // Privoxy
{ ".all", (true, false) },
{ ".aspx", (true, false) },
{ ".bat", (true, true) },
{ ".blacklist", (true, false) },
{ ".cidr", (true, true) },
{ ".conf", (true, true) }, // dnsmasq / Unbound / BIND
{ ".csv", (true, true) },
{ ".dat", (true, true) }, // Halite for Windows
{ ".deny", (true, true) },
{ ".gz", (false, false) },
{ ".hosts", (true, true) },
{ ".ips", (true, false) },
{ ".ipset", (true, true) }, // Firehol
{ ".json", (true, true) },
{ ".list", (true, false) },
{ ".lsrules", (true, true) }, // Little Snitch
{ ".md", (true, true) },
{ ".netset", (true, true) }, // Firehol
{ ".p2p", (true, true) }, // Peer Guardian
{ ".php", (true, false) },
{ ".raw", (true, false) },
{ ".rpz", (true, true) }, // Response Policy Zone
{ ".tpl", (true, true) }, // Internet Explorer
{ ".uBl", (true, false) },
{ ".zip", (false, false) },
{ ".zone", (true, false) }
};
private ListFileExtension(string value)
{
private static readonly IDictionary<string, (bool IsPlainText, bool IsMeaningfulToConsumer)> Info =
new Dictionary<string, (bool, bool)>
{
{ string.Empty, (true, false) },
{ ".txt", (true, false) },
{ ".7z", (false, false) },
{ ".action", (true, true) }, // Privoxy
{ ".all", (true, false) },
{ ".aspx", (true, false) },
{ ".bat", (true, true) },
{ ".blacklist", (true, false) },
{ ".cidr", (true, true) },
{ ".conf", (true, true) }, // dnsmasq / Unbound / BIND
{ ".csv", (true, true) },
{ ".dat", (true, true) }, // Halite for Windows
{ ".deny", (true, true) },
{ ".gz", (false, false) },
{ ".hosts", (true, true) },
{ ".ips", (true, false) },
{ ".ipset", (true, true) }, // Firehol
{ ".json", (true, true) },
{ ".list", (true, false) },
{ ".lsrules", (true, true) }, // Little Snitch
{ ".md", (true, true) },
{ ".netset", (true, true) }, // Firehol
{ ".p2p", (true, true) }, // Peer Guardian
{ ".php", (true, false) },
{ ".raw", (true, false) },
{ ".rpz", (true, true) }, // Response Policy Zone
{ ".tpl", (true, true) }, // Internet Explorer
{ ".uBl", (true, false) },
{ ".zip", (false, false) },
{ ".zone", (true, false) }
};
Value = value;
}
private ListFileExtension(string value)
{
Value = value;
}
public string Value { get; }
public string Value { get; }
public bool IsPlainText => Info[Value].IsPlainText;
public bool IsPlainText => Info[Value].IsPlainText;
public bool IsMeaningfulToConsumer => Info[Value].IsMeaningfulToConsumer;
public bool IsMeaningfulToConsumer => Info[Value].IsMeaningfulToConsumer;
public static ListFileExtension FromUri(Uri uri)
{
return new ListFileExtension(uri.GetFileExtension());
}
public static ListFileExtension FromUri(Uri uri)
{
return new ListFileExtension(uri.GetFileExtension());
}
protected override IEnumerable<object> GetEqualityComponents()
{
return new[] { Value };
}
protected override IEnumerable<object> GetEqualityComponents()
{
return new[] { Value };
}
}

View file

@ -1,7 +1,6 @@
namespace FilterLists.Archival.Domain.SeedWork
namespace FilterLists.Archival.Domain.SeedWork;
public interface IUnitOfWork : IDisposable
{
public interface IUnitOfWork : IDisposable
{
void Commit();
}
void Commit();
}

View file

@ -1,57 +1,56 @@
namespace FilterLists.Archival.Domain.SeedWork
namespace FilterLists.Archival.Domain.SeedWork;
/// <remarks>https://enterprisecraftsmanship.com/posts/value-object-better-implementation/</remarks>
public abstract class ValueObject
{
/// <remarks>https://enterprisecraftsmanship.com/posts/value-object-better-implementation/</remarks>
public abstract class ValueObject
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object? obj)
if (obj == null)
{
if (obj == null)
{
return false;
}
if (GetType() != obj.GetType())
{
return false;
}
var valueObject = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
return false;
}
public override int GetHashCode()
if (GetType() != obj.GetType())
{
return GetEqualityComponents()
.Aggregate(1, (current, obj) =>
return false;
}
var valueObject = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Aggregate(1, (current, obj) =>
{
unchecked
{
unchecked
{
return (current * 23) + (obj?.GetHashCode() ?? 0);
}
});
}
return (current * 23) + (obj?.GetHashCode() ?? 0);
}
});
}
public static bool operator ==(ValueObject? a, ValueObject? b)
public static bool operator ==(ValueObject? a, ValueObject? b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
{
return true;
}
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
{
return false;
}
return a.Equals(b);
return true;
}
public static bool operator !=(ValueObject? a, ValueObject? b)
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
{
return !(a == b);
return false;
}
return a.Equals(b);
}
public static bool operator !=(ValueObject? a, ValueObject? b)
{
return !(a == b);
}
}

View file

@ -1,17 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Polly;
namespace FilterLists.Archival.Infrastructure.Clients
namespace FilterLists.Archival.Infrastructure.Clients;
internal static class ConfigurationExtensions
{
internal static class ConfigurationExtensions
public static void AddClients(this IServiceCollection services)
{
public static void AddClients(this IServiceCollection services)
{
services.AddHttpClient<IHttpContentClient, HttpContentClient>()
.AddTransientHttpErrorPolicy(b => b.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)
}));
}
services.AddHttpClient<IHttpContentClient, HttpContentClient>()
.AddTransientHttpErrorPolicy(b => b.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)
}));
}
}

View file

@ -1,48 +1,47 @@
using Microsoft.Extensions.Logging;
namespace FilterLists.Archival.Infrastructure.Clients
namespace FilterLists.Archival.Infrastructure.Clients;
public interface IHttpContentClient : IDisposable
{
public interface IHttpContentClient : IDisposable
Task<Stream> GetContentAsync(Uri url, CancellationToken cancellationToken);
}
internal sealed class HttpContentClient : IHttpContentClient
{
private readonly HttpClient _httpClient;
private readonly ICollection<HttpResponseMessage> _httpResponseMessages = new HashSet<HttpResponseMessage>();
private readonly ILogger _logger;
public HttpContentClient(HttpClient httpClient, ILogger<HttpContentClient> logger)
{
Task<Stream> GetContentAsync(Uri url, CancellationToken cancellationToken);
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36");
_logger = logger;
}
internal sealed class HttpContentClient : IHttpContentClient
public async Task<Stream> GetContentAsync(Uri url, CancellationToken cancellationToken)
{
private readonly HttpClient _httpClient;
private readonly ICollection<HttpResponseMessage> _httpResponseMessages = new HashSet<HttpResponseMessage>();
private readonly ILogger _logger;
public HttpContentClient(HttpClient httpClient, ILogger<HttpContentClient> logger)
var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
_httpResponseMessages.Add(response);
try
{
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36");
_logger = logger;
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
public async Task<Stream> GetContentAsync(Uri url, CancellationToken cancellationToken)
catch (HttpRequestException ex)
{
var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
_httpResponseMessages.Add(response);
try
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to get content from {Url}", url);
return Stream.Null;
}
_logger.LogError(ex, "Failed to get content from {Url}", url);
return Stream.Null;
}
}
public void Dispose()
public void Dispose()
{
foreach (var message in _httpResponseMessages)
{
foreach (var message in _httpResponseMessages)
{
message.Dispose();
}
message.Dispose();
}
}
}

View file

@ -8,28 +8,27 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Archival.Infrastructure
namespace FilterLists.Archival.Infrastructure;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static IHostBuilder UseInfrastructure(this IHostBuilder hostBuilder)
{
public static IHostBuilder UseInfrastructure(this IHostBuilder hostBuilder)
{
return hostBuilder.UseLogging();
}
return hostBuilder.UseLogging();
}
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSharedKernelLogging(configuration);
services.AddSchedulingServices(configuration);
services.AddDirectoryApiClient(configuration);
services.AddClients();
services.AddPersistenceServices(configuration);
}
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSharedKernelLogging(configuration);
services.AddSchedulingServices(configuration);
services.AddDirectoryApiClient(configuration);
services.AddClients();
services.AddPersistenceServices(configuration);
}
public static void UseInfrastructure(this IApplicationBuilder app)
{
app.UseLogging();
app.UseScheduling();
}
public static void UseInfrastructure(this IApplicationBuilder app)
{
app.UseLogging();
app.UseScheduling();
}
}

View file

@ -1,11 +1,10 @@
namespace FilterLists.Archival.Infrastructure.Options
{
internal class GitOptions
{
public const string Key = "Git";
namespace FilterLists.Archival.Infrastructure.Options;
public string RepositoryPath { get; init; } = null!;
public string UserName { get; init; } = null!;
public string UserEmail { get; init; } = null!;
}
internal class GitOptions
{
public const string Key = "Git";
public string RepositoryPath { get; init; } = null!;
public string UserName { get; init; } = null!;
public string UserEmail { get; init; } = null!;
}

View file

@ -4,24 +4,23 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace FilterLists.Archival.Infrastructure.Persistence
{
internal static class ConfigurationExtensions
{
public static void AddPersistenceServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<GitOptions>(configuration.GetSection(GitOptions.Key));
services.AddTransient<IRepository, Repository>(_ =>
{
var gitOptions = configuration.GetSection(GitOptions.Key).Get<GitOptions>();
if (!Repository.IsValid(gitOptions.RepositoryPath))
{
Repository.Init(gitOptions.RepositoryPath);
}
namespace FilterLists.Archival.Infrastructure.Persistence;
return new Repository(gitOptions.RepositoryPath);
});
services.AddTransient<IListArchiveRepository, GitListArchiveRepository>();
}
internal static class ConfigurationExtensions
{
public static void AddPersistenceServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<GitOptions>(configuration.GetSection(GitOptions.Key));
services.AddTransient<IRepository, Repository>(_ =>
{
var gitOptions = configuration.GetSection(GitOptions.Key).Get<GitOptions>();
if (!Repository.IsValid(gitOptions.RepositoryPath))
{
Repository.Init(gitOptions.RepositoryPath);
}
return new Repository(gitOptions.RepositoryPath);
});
services.AddTransient<IListArchiveRepository, GitListArchiveRepository>();
}
}

View file

@ -1,9 +1,8 @@
using FilterLists.Archival.Domain.ListArchives;
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies;
internal interface IStreamToPlainTextConversionStrategy
{
internal interface IStreamToPlainTextConversionStrategy
{
Stream Convert(ListArchiveSegment listArchiveSegment, CancellationToken cancellationToken);
}
Stream Convert(ListArchiveSegment listArchiveSegment, CancellationToken cancellationToken);
}

View file

@ -1,12 +1,11 @@
using FilterLists.Archival.Domain.ListArchives;
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies;
internal class PlainText : IStreamToPlainTextConversionStrategy
{
internal class PlainText : IStreamToPlainTextConversionStrategy
public Stream Convert(ListArchiveSegment listArchiveSegment, CancellationToken cancellationToken)
{
public Stream Convert(ListArchiveSegment listArchiveSegment, CancellationToken cancellationToken)
{
return listArchiveSegment.Content;
}
return listArchiveSegment.Content;
}
}

View file

@ -1,16 +1,15 @@
using FilterLists.Archival.Domain.ListArchives;
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies
namespace FilterLists.Archival.Infrastructure.Persistence.FileWriteStrategies;
internal static class StreamToPlainTextConversionStrategyFactory
{
internal static class StreamToPlainTextConversionStrategyFactory
public static TStrategy? GetStrategy<TStrategy>(this ListArchiveSegment segment)
where TStrategy : class, IStreamToPlainTextConversionStrategy
{
public static TStrategy? GetStrategy<TStrategy>(this ListArchiveSegment segment)
where TStrategy : class, IStreamToPlainTextConversionStrategy
{
// TODO: implement non-plain text strategies
return segment.Extension.IsPlainText
? new PlainText() as TStrategy
: default;
}
// TODO: implement non-plain text strategies
return segment.Extension.IsPlainText
? new PlainText() as TStrategy
: default;
}
}

View file

@ -6,124 +6,123 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace FilterLists.Archival.Infrastructure.Persistence
namespace FilterLists.Archival.Infrastructure.Persistence;
internal sealed class GitListArchiveRepository : IListArchiveRepository
{
internal sealed class GitListArchiveRepository : IListArchiveRepository
private readonly ILogger _logger;
private readonly GitOptions _options;
private readonly IRepository _repo;
private readonly ICollection<FileInfo> _writtenFiles = new HashSet<FileInfo>();
public GitListArchiveRepository(
ILogger<GitListArchiveRepository> logger,
IOptions<GitOptions> gitOptions,
IRepository repository)
{
private readonly ILogger _logger;
private readonly GitOptions _options;
private readonly IRepository _repo;
private readonly ICollection<FileInfo> _writtenFiles = new HashSet<FileInfo>();
_logger = logger;
_options = gitOptions.Value;
_repo = repository;
}
public GitListArchiveRepository(
ILogger<GitListArchiveRepository> logger,
IOptions<GitOptions> gitOptions,
IRepository repository)
public async Task AddAsync(ListArchive listArchive, CancellationToken cancellationToken)
{
var segmentNumber = 1;
await foreach (var segment in listArchive.Segments.WithCancellation(cancellationToken))
{
_logger = logger;
_options = gitOptions.Value;
_repo = repository;
}
public async Task AddAsync(ListArchive listArchive, CancellationToken cancellationToken)
{
var segmentNumber = 1;
await foreach (var segment in listArchive.Segments.WithCancellation(cancellationToken))
var strategy = segment.GetStrategy<IStreamToPlainTextConversionStrategy>();
if (strategy is default(IStreamToPlainTextConversionStrategy))
{
var strategy = segment.GetStrategy<IStreamToPlainTextConversionStrategy>();
if (strategy is default(IStreamToPlainTextConversionStrategy))
{
_logger.LogWarning(
"No stream to plain text conversion strategy found for extension {Extension} for list {ListId}. Skipping list",
segment.Extension,
listArchive.Id);
return;
}
var fileInfo = GetTargetFile(listArchive.Id, segmentNumber, segment.Extension);
if (fileInfo is default(FileInfo))
{
_logger.LogWarning(
"Writing from non-plain text extension {Extension} for list {ListId} not yet supported. Skipping list",
segment.Extension,
listArchive.Id);
return;
}
_writtenFiles.Add(fileInfo);
_logger.LogInformation("Writing {FileName}", fileInfo.Name);
await using var target = fileInfo.OpenWrite();
await strategy.Convert(segment, cancellationToken).CopyToAsync(target, cancellationToken);
_logger.LogInformation("Finished writing {FileName}", fileInfo.Name);
segmentNumber++;
}
}
public void Commit()
{
if (_writtenFiles.Count > 0)
{
var fileNames = _writtenFiles.Select(f => f.Name).ToList();
var signature = new Signature(_options.UserName, _options.UserEmail, DateTime.UtcNow);
var message =
$"feat(archives): archive {fileNames.Count} file(s){Environment.NewLine}{string.Join(Environment.NewLine, fileNames)}";
Commands.Stage(_repo, fileNames);
try
{
_repo.Commit(message, signature, signature);
}
catch (EmptyCommitException ex)
{
_logger.LogInformation(ex, "No changes to commit for {@FileNames}", fileNames);
}
_logger.LogInformation("Committed {@FileNames}", fileNames);
}
else
{
_logger.LogInformation("No written files to commit");
}
}
public void Dispose()
{
foreach (var file in _writtenFiles)
{
if (File.Exists(file.Name))
{
File.Delete(file.Name);
}
_logger.LogWarning(
"No stream to plain text conversion strategy found for extension {Extension} for list {ListId}. Skipping list",
segment.Extension,
listArchive.Id);
return;
}
_repo.CheckoutPaths("HEAD", _writtenFiles.Select(f => f.Name));
}
private FileInfo? GetTargetFile(int listId, int segmentNumber, ListFileExtension extension)
{
string targetExtension;
if (extension.IsPlainText)
var fileInfo = GetTargetFile(listArchive.Id, segmentNumber, segment.Extension);
if (fileInfo is default(FileInfo))
{
targetExtension = extension.IsMeaningfulToConsumer ? extension.Value : ".txt";
}
else
{
// TODO: implement
return default;
_logger.LogWarning(
"Writing from non-plain text extension {Extension} for list {ListId} not yet supported. Skipping list",
segment.Extension,
listArchive.Id);
return;
}
var targetFileName = GetTargetFileNamePrefix(listId) +
(segmentNumber == 1 ? string.Empty : $"-{segmentNumber}") +
targetExtension;
return new FileInfo(Path.Combine(_options.RepositoryPath, targetFileName));
}
_writtenFiles.Add(fileInfo);
private static string GetTargetFileNamePrefix(int listId)
{
return listId.ToString(CultureInfo.InvariantCulture).PadLeft(5, '0');
_logger.LogInformation("Writing {FileName}", fileInfo.Name);
await using var target = fileInfo.OpenWrite();
await strategy.Convert(segment, cancellationToken).CopyToAsync(target, cancellationToken);
_logger.LogInformation("Finished writing {FileName}", fileInfo.Name);
segmentNumber++;
}
}
public void Commit()
{
if (_writtenFiles.Count > 0)
{
var fileNames = _writtenFiles.Select(f => f.Name).ToList();
var signature = new Signature(_options.UserName, _options.UserEmail, DateTime.UtcNow);
var message =
$"feat(archives): archive {fileNames.Count} file(s){Environment.NewLine}{string.Join(Environment.NewLine, fileNames)}";
Commands.Stage(_repo, fileNames);
try
{
_repo.Commit(message, signature, signature);
}
catch (EmptyCommitException ex)
{
_logger.LogInformation(ex, "No changes to commit for {@FileNames}", fileNames);
}
_logger.LogInformation("Committed {@FileNames}", fileNames);
}
else
{
_logger.LogInformation("No written files to commit");
}
}
public void Dispose()
{
foreach (var file in _writtenFiles)
{
if (File.Exists(file.Name))
{
File.Delete(file.Name);
}
}
_repo.CheckoutPaths("HEAD", _writtenFiles.Select(f => f.Name));
}
private FileInfo? GetTargetFile(int listId, int segmentNumber, ListFileExtension extension)
{
string targetExtension;
if (extension.IsPlainText)
{
targetExtension = extension.IsMeaningfulToConsumer ? extension.Value : ".txt";
}
else
{
// TODO: implement
return default;
}
var targetFileName = GetTargetFileNamePrefix(listId) +
(segmentNumber == 1 ? string.Empty : $"-{segmentNumber}") +
targetExtension;
return new FileInfo(Path.Combine(_options.RepositoryPath, targetFileName));
}
private static string GetTargetFileNamePrefix(int listId)
{
return listId.ToString(CultureInfo.InvariantCulture).PadLeft(5, '0');
}
}

View file

@ -4,21 +4,20 @@
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
namespace FilterLists.Archival.Infrastructure.Scheduling
namespace FilterLists.Archival.Infrastructure.Scheduling;
internal static class ConfigurationExtensions
{
internal static class ConfigurationExtensions
private static ConnectionMultiplexer _redis = null!;
public static void AddSchedulingServices(this IServiceCollection services, IConfiguration configuration)
{
private static ConnectionMultiplexer _redis = null!;
_redis = ConnectionMultiplexer.Connect(configuration.GetConnectionString("SchedulingConnection"));
services.AddHangfire((_, globalConfiguration) => globalConfiguration.UseRedisStorage(_redis).UseMediatR());
}
public static void AddSchedulingServices(this IServiceCollection services, IConfiguration configuration)
{
_redis = ConnectionMultiplexer.Connect(configuration.GetConnectionString("SchedulingConnection"));
services.AddHangfire((_, globalConfiguration) => globalConfiguration.UseRedisStorage(_redis).UseMediatR());
}
public static void UseScheduling(this IApplicationBuilder app)
{
app.UseHangfireServer(new BackgroundJobServerOptions { WorkerCount = Environment.ProcessorCount });
}
public static void UseScheduling(this IApplicationBuilder app)
{
app.UseHangfireServer(new BackgroundJobServerOptions { WorkerCount = Environment.ProcessorCount });
}
}

View file

@ -1,26 +1,25 @@
using Hangfire;
using Hangfire.Storage;
namespace FilterLists.Archival.Infrastructure.Scheduling
{
public static class HangfireExtensions
{
// https://github.com/HangfireIO/Hangfire/issues/394#issuecomment-179924221
public static void PurgeJobs(this IMonitoringApi monitoringApi)
{
var toDelete = new List<string>();
foreach (var queue in monitoringApi.Queues())
{
for (var i = 0; i < Math.Ceiling(queue.Length / 1000d); i++)
{
toDelete.AddRange(monitoringApi.EnqueuedJobs(queue.Name, 1000 * i, 1000).Select(x => x.Key));
}
}
namespace FilterLists.Archival.Infrastructure.Scheduling;
foreach (var jobId in toDelete)
public static class HangfireExtensions
{
// https://github.com/HangfireIO/Hangfire/issues/394#issuecomment-179924221
public static void PurgeJobs(this IMonitoringApi monitoringApi)
{
var toDelete = new List<string>();
foreach (var queue in monitoringApi.Queues())
{
for (var i = 0; i < Math.Ceiling(queue.Length / 1000d); i++)
{
BackgroundJob.Delete(jobId);
toDelete.AddRange(monitoringApi.EnqueuedJobs(queue.Name, 1000 * i, 1000).Select(x => x.Key));
}
}
foreach (var jobId in toDelete)
{
BackgroundJob.Delete(jobId);
}
}
}

View file

@ -2,36 +2,35 @@
using MediatR;
using Newtonsoft.Json;
namespace FilterLists.Archival.Infrastructure.Scheduling
namespace FilterLists.Archival.Infrastructure.Scheduling;
public static class RequestExtensions
{
public static class RequestExtensions
public static void EnqueueBackgroundJob(this IRequest request)
{
public static void EnqueueBackgroundJob(this IRequest request)
{
// Hangfire replaces CancellationToken at runtime with its own. We just need any in the signature.
// https://docs.hangfire.io/en/latest/background-methods/using-cancellation-tokens.html#cancellationtoken
BackgroundJob.Enqueue<IMediator>(m => m.Send(request, CancellationToken.None));
}
public static void ScheduleBackgroundJob(this IRequest request, TimeSpan delay)
{
BackgroundJob.Schedule<IMediator>(m => m.Send(request, CancellationToken.None), delay);
}
public static void AddOrUpdateRecurringJob(this IRequest request, Func<string> cronExpression)
{
RecurringJob.AddOrUpdate<IMediator>(m => m.Send(request, CancellationToken.None), cronExpression);
}
// Hangfire replaces CancellationToken at runtime with its own. We just need any in the signature.
// https://docs.hangfire.io/en/latest/background-methods/using-cancellation-tokens.html#cancellationtoken
BackgroundJob.Enqueue<IMediator>(m => m.Send(request, CancellationToken.None));
}
internal static class HangfireExtension
public static void ScheduleBackgroundJob(this IRequest request, TimeSpan delay)
{
public static IGlobalConfiguration UseMediatR(this IGlobalConfiguration globalConfiguration)
{
/// https://codeopinion.com/background-commands-mediatr-hangfire/
GlobalConfiguration.Configuration.UseSerializerSettings(
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects });
return globalConfiguration;
}
BackgroundJob.Schedule<IMediator>(m => m.Send(request, CancellationToken.None), delay);
}
public static void AddOrUpdateRecurringJob(this IRequest request, Func<string> cronExpression)
{
RecurringJob.AddOrUpdate<IMediator>(m => m.Send(request, CancellationToken.None), cronExpression);
}
}
internal static class HangfireExtension
{
public static IGlobalConfiguration UseMediatR(this IGlobalConfiguration globalConfiguration)
{
/// https://codeopinion.com/background-commands-mediatr-hangfire/
GlobalConfiguration.Configuration.UseSerializerSettings(
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects });
return globalConfiguration;
}
}

View file

@ -4,24 +4,23 @@
using Polly;
using Refit;
namespace FilterLists.Directory.Api.Contracts
namespace FilterLists.Directory.Api.Contracts;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static void AddDirectoryApiClient(this IServiceCollection services, IConfiguration configuration)
{
public static void AddDirectoryApiClient(this IServiceCollection services, IConfiguration configuration)
{
// TODO: use SystemTextJsonContentSerializer() once less feature-limited
services.AddRefitClient<IDirectoryApi>()
.ConfigureHttpClient(c =>
// TODO: use SystemTextJsonContentSerializer() once less feature-limited
services.AddRefitClient<IDirectoryApi>()
.ConfigureHttpClient(c =>
{
var host = configuration.GetSection(ApiOptions.Key).Get<ApiOptions>().DirectoryHost;
c.BaseAddress = new UriBuilder("http", host).Uri;
})
.AddTransientHttpErrorPolicy(b =>
b.WaitAndRetryAsync(new[]
{
var host = configuration.GetSection(ApiOptions.Key).Get<ApiOptions>().DirectoryHost;
c.BaseAddress = new UriBuilder("http", host).Uri;
})
.AddTransientHttpErrorPolicy(b =>
b.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)
}));
}
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)
}));
}
}

View file

@ -1,14 +1,13 @@
using FilterLists.Directory.Api.Contracts.Models;
using Refit;
namespace FilterLists.Directory.Api.Contracts
{
public interface IDirectoryApi
{
[Get("/lists")]
Task<IEnumerable<ListVm>> GetListsAsync(CancellationToken cancellationToken);
namespace FilterLists.Directory.Api.Contracts;
[Get("/lists/{id}")]
Task<ListDetailsVm> GetListDetailsAsync(int id, CancellationToken cancellationToken);
}
public interface IDirectoryApi
{
[Get("/lists")]
Task<IEnumerable<ListVm>> GetListsAsync(CancellationToken cancellationToken);
[Get("/lists/{id}")]
Task<ListDetailsVm> GetListDetailsAsync(int id, CancellationToken cancellationToken);
}

View file

@ -1,37 +1,36 @@
namespace FilterLists.Directory.Api.Contracts.Models
{
public class ListDetailsVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
public IEnumerable<string>? Iso6391s { get; init; }
public IEnumerable<int>? TagIds { get; init; }
public IEnumerable<ViewUrlVm>? ViewUrls { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? OnionUrl { get; init; }
public Uri? PolicyUrl { get; init; }
public Uri? SubmissionUrl { get; init; }
public Uri? IssuesUrl { get; init; }
public Uri? ForumUrl { get; init; }
public Uri? ChatUrl { get; init; }
public string? EmailAddress { get; init; }
public Uri? DonateUrl { get; init; }
public IEnumerable<int>? MaintainerIds { get; init; }
public IEnumerable<int>? UpstreamFilterListIds { get; init; }
public IEnumerable<int>? ForkFilterListIds { get; init; }
public IEnumerable<int>? IncludedInFilterListIds { get; init; }
public IEnumerable<int>? IncludesFilterListIds { get; init; }
public IEnumerable<int>? DependencyFilterListIds { get; init; }
public IEnumerable<int>? DependentFilterListIds { get; init; }
namespace FilterLists.Directory.Api.Contracts.Models;
public class ViewUrlVm
{
public short SegmentNumber { get; init; }
public short Primariness { get; init; }
public Uri Url { get; init; } = null!;
}
public class ListDetailsVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
public IEnumerable<string>? Iso6391s { get; init; }
public IEnumerable<int>? TagIds { get; init; }
public IEnumerable<ViewUrlVm>? ViewUrls { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? OnionUrl { get; init; }
public Uri? PolicyUrl { get; init; }
public Uri? SubmissionUrl { get; init; }
public Uri? IssuesUrl { get; init; }
public Uri? ForumUrl { get; init; }
public Uri? ChatUrl { get; init; }
public string? EmailAddress { get; init; }
public Uri? DonateUrl { get; init; }
public IEnumerable<int>? MaintainerIds { get; init; }
public IEnumerable<int>? UpstreamFilterListIds { get; init; }
public IEnumerable<int>? ForkFilterListIds { get; init; }
public IEnumerable<int>? IncludedInFilterListIds { get; init; }
public IEnumerable<int>? IncludesFilterListIds { get; init; }
public IEnumerable<int>? DependencyFilterListIds { get; init; }
public IEnumerable<int>? DependentFilterListIds { get; init; }
public class ViewUrlVm
{
public short SegmentNumber { get; init; }
public short Primariness { get; init; }
public Uri Url { get; init; } = null!;
}
}

View file

@ -1,15 +1,14 @@
namespace FilterLists.Directory.Api.Contracts.Models
namespace FilterLists.Directory.Api.Contracts.Models;
public class ListVm
{
public class ListVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
public IEnumerable<string>? Iso6391s { get; init; }
public IEnumerable<int>? TagIds { get; init; }
public Uri? PrimaryViewUrl { get; init; }
public IEnumerable<int>? MaintainerIds { get; init; }
}
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
public IEnumerable<string>? Iso6391s { get; init; }
public IEnumerable<int>? TagIds { get; init; }
public Uri? PrimaryViewUrl { get; init; }
public IEnumerable<int>? MaintainerIds { get; init; }
}

View file

@ -1,9 +1,8 @@
namespace FilterLists.Directory.Api.Contracts.Options
{
internal class ApiOptions
{
public const string Key = "Api";
namespace FilterLists.Directory.Api.Contracts.Options;
public string DirectoryHost { get; init; } = null!;
}
internal class ApiOptions
{
public const string Key = "Api";
public string DirectoryHost { get; init; } = null!;
}

View file

@ -2,34 +2,33 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public abstract class BaseController : ControllerBase
{
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public abstract class BaseController : ControllerBase
private readonly IMemoryCache _cache;
protected BaseController(IMemoryCache cache)
{
private readonly IMemoryCache _cache;
_cache = cache;
}
protected BaseController(IMemoryCache cache)
/// <remarks>https://stackoverflow.com/a/52506210/2343739</remarks>
protected async Task<IActionResult> CacheGetOrCreateAsync<TResponse>(
Func<Task<TResponse>> actionAsync,
int? keySuffix = default,
TimeSpan? absoluteExpirationRelativeToNow = default,
[CallerMemberName] string key = default!)
{
var cacheKey = $"{GetType().Name}_{key}{(keySuffix is null ? string.Empty : $"_{keySuffix}")}";
var result = await _cache.GetOrCreateAsync(cacheKey, entry =>
{
_cache = cache;
}
/// <remarks>https://stackoverflow.com/a/52506210/2343739</remarks>
protected async Task<IActionResult> CacheGetOrCreateAsync<TResponse>(
Func<Task<TResponse>> actionAsync,
int? keySuffix = default,
TimeSpan? absoluteExpirationRelativeToNow = default,
[CallerMemberName] string key = default!)
{
var cacheKey = $"{GetType().Name}_{key}{(keySuffix is null ? string.Empty : $"_{keySuffix}")}";
var result = await _cache.GetOrCreateAsync(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
return actionAsync();
});
return result is null ? NotFound() : Ok(result);
}
entry.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
return actionAsync();
});
return result is null ? NotFound() : Ok(result);
}
}

View file

@ -1,29 +1,28 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
// TODO: de-duplicate into SharedKernel
public class ErrorController : ControllerBase
{
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
// TODO: de-duplicate into SharedKernel
public class ErrorController : ControllerBase
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment([FromServices] IWebHostEnvironment webHostEnvironment)
{
[Route("/error-local-development")]
public IActionResult ErrorLocalDevelopment([FromServices] IWebHostEnvironment webHostEnvironment)
if (webHostEnvironment.EnvironmentName != "Development")
{
if (webHostEnvironment.EnvironmentName != "Development")
{
throw new InvalidOperationException("This shouldn't be invoked in non-development environments.");
}
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(context?.Error.StackTrace, title: context?.Error.Message);
throw new InvalidOperationException("This shouldn't be invoked in non-development environments.");
}
[Route("/error")]
public IActionResult Error()
{
return Problem();
}
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
return Problem(context?.Error.StackTrace, title: context?.Error.Message);
}
[Route("/error")]
public IActionResult Error()
{
return Problem();
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class LanguagesController : BaseController
{
public class LanguagesController : BaseController
private readonly IMediator _mediator;
public LanguagesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public LanguagesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the languages targeted by FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The languages targeted by FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetLanguages.LanguageVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLanguages.Query(), cancellationToken));
}
/// <summary>
/// Gets the languages targeted by FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The languages targeted by FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetLanguages.LanguageVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLanguages.Query(), cancellationToken));
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class LicensesController : BaseController
{
public class LicensesController : BaseController
private readonly IMediator _mediator;
public LicensesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public LicensesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the licenses applied to FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The licenses applied to FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetLicenses.LicenseVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLicenses.Query(), cancellationToken));
}
/// <summary>
/// Gets the licenses applied to FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The licenses applied to FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetLicenses.LicenseVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLicenses.Query(), cancellationToken));
}
}

View file

@ -4,41 +4,40 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class ListsController : BaseController
{
public class ListsController : BaseController
private readonly IMediator _mediator;
public ListsController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public ListsController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the FilterLists..
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ListVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLists.Query(), cancellationToken));
}
/// <summary>
/// Gets the FilterLists..
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<ListVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLists.Query(), cancellationToken));
}
/// <summary>
/// Gets the details of the FilterList.
/// </summary>
/// <param name="id">The identifier of the FilterList.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The details of the FilterList.</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ListDetailsVm), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetDetails(int id, CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetListDetails.Query(id), cancellationToken), id);
}
/// <summary>
/// Gets the details of the FilterList.
/// </summary>
/// <param name="id">The identifier of the FilterList.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The details of the FilterList.</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ListDetailsVm), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> GetDetails(int id, CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetListDetails.Query(id), cancellationToken), id);
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class MaintainersController : BaseController
{
public class MaintainersController : BaseController
private readonly IMediator _mediator;
public MaintainersController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public MaintainersController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the maintainers of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The maintainers of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetMaintainers.MaintainerVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetMaintainers.Query(), cancellationToken));
}
/// <summary>
/// Gets the maintainers of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The maintainers of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetMaintainers.MaintainerVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetMaintainers.Query(), cancellationToken));
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class SoftwareController : BaseController
{
public class SoftwareController : BaseController
private readonly IMediator _mediator;
public SoftwareController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public SoftwareController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the software that subscribes to FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The software that subscribes to FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetSoftware.SoftwareVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetSoftware.Query(), cancellationToken));
}
/// <summary>
/// Gets the software that subscribes to FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The software that subscribes to FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetSoftware.SoftwareVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetSoftware.Query(), cancellationToken));
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class SyntaxesController : BaseController
{
public class SyntaxesController : BaseController
private readonly IMediator _mediator;
public SyntaxesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public SyntaxesController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the syntaxes of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The syntaxes of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetSyntaxes.SyntaxVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetSyntaxes.Query(), cancellationToken));
}
/// <summary>
/// Gets the syntaxes of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The syntaxes of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetSyntaxes.SyntaxVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetSyntaxes.Query(), cancellationToken));
}
}

View file

@ -3,27 +3,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FilterLists.Directory.Api.Controllers
namespace FilterLists.Directory.Api.Controllers;
public class TagsController : BaseController
{
public class TagsController : BaseController
private readonly IMediator _mediator;
public TagsController(IMemoryCache cache, IMediator mediator) : base(cache)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public TagsController(IMemoryCache cache, IMediator mediator) : base(cache)
{
_mediator = mediator;
}
/// <summary>
/// Gets the tags of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tags of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetTags.TagVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetTags.Query(), cancellationToken));
}
/// <summary>
/// Gets the tags of FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The tags of FilterLists.</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<GetTags.TagVm>), StatusCodes.Status200OK)]
public Task<IActionResult> Get(CancellationToken cancellationToken)
{
return CacheGetOrCreateAsync(() => _mediator.Send(new GetTags.Query(), cancellationToken));
}
}

View file

@ -2,22 +2,21 @@
using FilterLists.Directory.Infrastructure.Persistence;
using FilterLists.SharedKernel.Logging;
namespace FilterLists.Directory.Api
{
public static class Program
{
// TODO: migrate to new hosting model https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60?view=aspnetcore-6.0&tabs=visual-studio#new-hosting-model
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.TryRunWithLoggingAsync(async () => await host.MigrateAsync());
}
namespace FilterLists.Directory.Api;
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseApplication()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
public static class Program
{
// TODO: migrate to new hosting model https://docs.microsoft.com/en-us/aspnet/core/migration/50-to-60?view=aspnetcore-6.0&tabs=visual-studio#new-hosting-model
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.TryRunWithLoggingAsync(async () => await host.MigrateAsync());
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseApplication()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
}

View file

@ -1,34 +1,33 @@
using System.Text.Json.Serialization;
using FilterLists.Directory.Application;
namespace FilterLists.Directory.Api
namespace FilterLists.Directory.Api;
internal class Startup
{
internal class Startup
public Startup(IConfiguration configuration)
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddRouting(o => o.LowercaseUrls = true);
services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull);
services.AddSwaggerGen();
services.AddApplicationServices(Configuration);
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddRouting(o => o.LowercaseUrls = true);
services.AddControllers().AddJsonOptions(o =>
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull);
services.AddSwaggerGen();
services.AddApplicationServices(Configuration);
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseApplication();
app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
app.UseSwagger();
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseApplication();
app.UseExceptionHandler(env.IsDevelopment() ? "/error-local-development" : "/error");
app.UseRouting();
app.UseEndpoints(e => e.MapControllers());
app.UseSwagger();
}
}

View file

@ -1,44 +1,43 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
namespace FilterLists.Directory.Api
namespace FilterLists.Directory.Api;
internal static class SwaggerExtensions
{
internal static class SwaggerExtensions
public static void AddSwaggerGen(this IServiceCollection services)
{
public static void AddSwaggerGen(this IServiceCollection services)
services.AddSwaggerGen(o =>
{
services.AddSwaggerGen(o =>
o.SwaggerDoc("v1", new OpenApiInfo
{
o.SwaggerDoc("v1", new OpenApiInfo
Title = "FilterLists Directory API",
Description = "An ASP.NET Core API serving the core FilterList information.",
Version = "v1",
//TermsOfService = "",
Contact = new OpenApiContact { Name = "FilterLists", Url = new Uri("https://filterlists.com") },
License = new OpenApiLicense
{
Title = "FilterLists Directory API",
Description = "An ASP.NET Core API serving the core FilterList information.",
Version = "v1",
//TermsOfService = "",
Contact = new OpenApiContact { Name = "FilterLists", Url = new Uri("https://filterlists.com") },
License = new OpenApiLicense
{
Name = "MIT License",
Url = new Uri("https://github.com/collinbarrett/FilterLists/blob/master/LICENSE")
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
o.IncludeXmlComments(xmlPath);
Name = "MIT License",
Url = new Uri("https://github.com/collinbarrett/FilterLists/blob/master/LICENSE")
}
});
}
public static void UseSwagger(this IApplicationBuilder app)
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
o.IncludeXmlComments(xmlPath);
});
}
public static void UseSwagger(this IApplicationBuilder app)
{
app.UseSwagger(o =>
{
app.UseSwagger(o =>
o.RouteTemplate = "{documentName}/swagger.json";
o.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List<OpenApiServer>
{
o.RouteTemplate = "{documentName}/swagger.json";
o.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.Servers = new List<OpenApiServer>
{
new() { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/api/directory" }
});
new() { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}/api/directory" }
});
}
});
}
}

View file

@ -5,25 +5,24 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Directory.Application
namespace FilterLists.Directory.Application;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static IHostBuilder UseApplication(this IHostBuilder hostBuilder)
{
public static IHostBuilder UseApplication(this IHostBuilder hostBuilder)
{
return hostBuilder.UseInfrastructure();
}
return hostBuilder.UseInfrastructure();
}
public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(ConfigurationExtensions).Assembly);
services.AddAutoMapper(typeof(ConfigurationExtensions).Assembly);
services.AddInfrastructureServices(configuration);
}
public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(ConfigurationExtensions).Assembly);
services.AddAutoMapper(typeof(ConfigurationExtensions).Assembly);
services.AddInfrastructureServices(configuration);
}
public static void UseApplication(this IApplicationBuilder app)
{
app.UseInfrastructure();
}
public static void UseApplication(this IApplicationBuilder app)
{
app.UseInfrastructure();
}
}

View file

@ -5,54 +5,53 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetLanguages
{
public static class GetLanguages
public class Query : IRequest<IEnumerable<LanguageVm>>
{
public class Query : IRequest<IEnumerable<LanguageVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<LanguageVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<LanguageVm>>
public async Task<IEnumerable<LanguageVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<LanguageVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Languages
.Where(l => l.FilterListLanguages.Any())
.OrderBy(l => l.Iso6391)
.ProjectTo<LanguageVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class LanguageVmProfile : Profile
{
public LanguageVmProfile()
{
CreateMap<Language, LanguageVm>()
.ForMember(l => l.FilterListIds,
o => o.MapFrom(l =>
l.FilterListLanguages.Select(fll => fll.FilterListId).OrderBy(flid => flid)
.AsEnumerable()));
}
}
public class LanguageVm
{
public string Iso6391 { get; init; } = null!;
public string Name { get; init; } = null!;
public IEnumerable<int>? FilterListIds { get; init; }
return await _context.Languages
.Where(l => l.FilterListLanguages.Any())
.OrderBy(l => l.Iso6391)
.ProjectTo<LanguageVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class LanguageVmProfile : Profile
{
public LanguageVmProfile()
{
CreateMap<Language, LanguageVm>()
.ForMember(l => l.FilterListIds,
o => o.MapFrom(l =>
l.FilterListLanguages.Select(fll => fll.FilterListId).OrderBy(flid => flid)
.AsEnumerable()));
}
}
public class LanguageVm
{
public string Iso6391 { get; init; } = null!;
public string Name { get; init; } = null!;
public IEnumerable<int>? FilterListIds { get; init; }
}
}

View file

@ -5,56 +5,55 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetLicenses
{
public static class GetLicenses
public class Query : IRequest<IEnumerable<LicenseVm>>
{
public class Query : IRequest<IEnumerable<LicenseVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<LicenseVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<LicenseVm>>
public async Task<IEnumerable<LicenseVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<LicenseVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Licenses
.OrderBy(l => l.Id)
.ProjectTo<LicenseVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class LicenseVmProfile : Profile
{
public LicenseVmProfile()
{
CreateMap<License, LicenseVm>()
.ForMember(l => l.FilterListIds,
o => o.MapFrom(l =>
l.FilterLists.Select(fl => fl.Id).OrderBy(flid => flid).AsEnumerable()));
}
}
public class LicenseVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public bool PermitsModification { get; init; }
public bool PermitsDistribution { get; init; }
public bool PermitsCommercialUse { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
return await _context.Licenses
.OrderBy(l => l.Id)
.ProjectTo<LicenseVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class LicenseVmProfile : Profile
{
public LicenseVmProfile()
{
CreateMap<License, LicenseVm>()
.ForMember(l => l.FilterListIds,
o => o.MapFrom(l =>
l.FilterLists.Select(fl => fl.Id).OrderBy(flid => flid).AsEnumerable()));
}
}
public class LicenseVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public bool PermitsModification { get; init; }
public bool PermitsDistribution { get; init; }
public bool PermitsCommercialUse { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
}
}

View file

@ -6,89 +6,88 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetListDetails
{
public static class GetListDetails
public class Query : IRequest<ListDetailsVm?>
{
public class Query : IRequest<ListDetailsVm?>
public Query(int id)
{
public Query(int id)
{
Id = id;
}
public int Id { get; }
Id = id;
}
internal class Handler : IRequestHandler<Query, ListDetailsVm?>
public int Id { get; }
}
internal class Handler : IRequestHandler<Query, ListDetailsVm?>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<ListDetailsVm?> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.FilterLists
.ProjectTo<ListDetailsVm>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync(fl => fl.Id == request.Id, cancellationToken);
}
_context = context;
_mapper = mapper;
}
internal class ListDetailsVmProfile : Profile
public async Task<ListDetailsVm?> Handle(
Query request,
CancellationToken cancellationToken)
{
public ListDetailsVmProfile()
{
CreateMap<FilterList, ListDetailsVm>()
.ForMember(fl => fl.SyntaxIds,
o => o.MapFrom(fl =>
fl.FilterListSyntaxes.Select(fls => fls.SyntaxId).OrderBy(sid => sid).AsEnumerable()))
.ForMember(fl => fl.Iso6391s,
o => o.MapFrom(fl =>
fl.FilterListLanguages.Select(fls => fls.Iso6391).OrderBy(i => i).AsEnumerable()))
.ForMember(fl => fl.TagIds,
o => o.MapFrom(fl =>
fl.FilterListTags.Select(flt => flt.TagId).OrderBy(tid => tid).AsEnumerable()))
.ForMember(fl => fl.ViewUrls,
o => o.MapFrom(fl =>
fl.ViewUrls.OrderBy(u => u.SegmentNumber).ThenBy(u => u.Primariness).AsEnumerable()))
.ForMember(fl => fl.MaintainerIds,
o => o.MapFrom(fl =>
fl.FilterListMaintainers.Select(flm => flm.MaintainerId).OrderBy(mid => mid)
.AsEnumerable()))
.ForMember(fl => fl.UpstreamFilterListIds,
o => o.MapFrom(fl =>
fl.UpstreamFilterLists.Select(ufl => ufl.UpstreamFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.ForkFilterListIds,
o => o.MapFrom(fl =>
fl.ForkFilterLists.Select(ffl => ffl.ForkFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.IncludedInFilterListIds,
o => o.MapFrom(fl =>
fl.IncludedInFilterLists.Select(iifl => iifl.IncludedInFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.IncludesFilterListIds,
o => o.MapFrom(fl =>
fl.IncludesFilterLists.Select(ifl => ifl.IncludesFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.DependencyFilterListIds,
o => o.MapFrom(fl =>
fl.DependencyFilterLists.Select(dfl => dfl.DependencyFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.DependentFilterListIds,
o => o.MapFrom(fl =>
fl.DependentFilterLists.Select(dfl => dfl.DependentFilterListId).OrderBy(flid => flid)
.AsEnumerable()));
return await _context.FilterLists
.ProjectTo<ListDetailsVm>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync(fl => fl.Id == request.Id, cancellationToken);
}
}
CreateMap<FilterListViewUrl, ListDetailsVm.ViewUrlVm>();
}
internal class ListDetailsVmProfile : Profile
{
public ListDetailsVmProfile()
{
CreateMap<FilterList, ListDetailsVm>()
.ForMember(fl => fl.SyntaxIds,
o => o.MapFrom(fl =>
fl.FilterListSyntaxes.Select(fls => fls.SyntaxId).OrderBy(sid => sid).AsEnumerable()))
.ForMember(fl => fl.Iso6391s,
o => o.MapFrom(fl =>
fl.FilterListLanguages.Select(fls => fls.Iso6391).OrderBy(i => i).AsEnumerable()))
.ForMember(fl => fl.TagIds,
o => o.MapFrom(fl =>
fl.FilterListTags.Select(flt => flt.TagId).OrderBy(tid => tid).AsEnumerable()))
.ForMember(fl => fl.ViewUrls,
o => o.MapFrom(fl =>
fl.ViewUrls.OrderBy(u => u.SegmentNumber).ThenBy(u => u.Primariness).AsEnumerable()))
.ForMember(fl => fl.MaintainerIds,
o => o.MapFrom(fl =>
fl.FilterListMaintainers.Select(flm => flm.MaintainerId).OrderBy(mid => mid)
.AsEnumerable()))
.ForMember(fl => fl.UpstreamFilterListIds,
o => o.MapFrom(fl =>
fl.UpstreamFilterLists.Select(ufl => ufl.UpstreamFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.ForkFilterListIds,
o => o.MapFrom(fl =>
fl.ForkFilterLists.Select(ffl => ffl.ForkFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.IncludedInFilterListIds,
o => o.MapFrom(fl =>
fl.IncludedInFilterLists.Select(iifl => iifl.IncludedInFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.IncludesFilterListIds,
o => o.MapFrom(fl =>
fl.IncludesFilterLists.Select(ifl => ifl.IncludesFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.DependencyFilterListIds,
o => o.MapFrom(fl =>
fl.DependencyFilterLists.Select(dfl => dfl.DependencyFilterListId).OrderBy(flid => flid)
.AsEnumerable()))
.ForMember(fl => fl.DependentFilterListIds,
o => o.MapFrom(fl =>
fl.DependentFilterLists.Select(dfl => dfl.DependentFilterListId).OrderBy(flid => flid)
.AsEnumerable()));
CreateMap<FilterListViewUrl, ListDetailsVm.ViewUrlVm>();
}
}
}

View file

@ -6,59 +6,58 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetLists
{
public static class GetLists
public class Query : IRequest<IEnumerable<ListVm>>
{
public class Query : IRequest<IEnumerable<ListVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<ListVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<ListVm>>
public async Task<IEnumerable<ListVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<ListVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.FilterLists
.OrderBy(fl => fl.Id)
.ProjectTo<ListVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
return await _context.FilterLists
.OrderBy(fl => fl.Id)
.ProjectTo<ListVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class ListVmProfile : Profile
internal class ListVmProfile : Profile
{
public ListVmProfile()
{
public ListVmProfile()
{
CreateMap<FilterList, ListVm>()
.ForMember(fl => fl.SyntaxIds,
o => o.MapFrom(fl =>
fl.FilterListSyntaxes.Select(fls => fls.SyntaxId).OrderBy(sid => sid).AsEnumerable()))
.ForMember(fl => fl.Iso6391s,
o => o.MapFrom(fl =>
fl.FilterListLanguages.Select(fls => fls.Iso6391).OrderBy(i => i).AsEnumerable()))
.ForMember(fl => fl.TagIds,
o => o.MapFrom(fl =>
fl.FilterListTags.Select(flt => flt.TagId).OrderBy(tid => tid).AsEnumerable()))
.ForMember(fl => fl.PrimaryViewUrl,
o => o.MapFrom(fl =>
fl.ViewUrls.OrderBy(u => u.SegmentNumber).ThenBy(u => u.Primariness).Select(u => u.Url)
.FirstOrDefault()))
.ForMember(fl => fl.MaintainerIds,
o => o.MapFrom(fl =>
fl.FilterListMaintainers.Select(flm => flm.MaintainerId).OrderBy(mid => mid)
.AsEnumerable()));
}
CreateMap<FilterList, ListVm>()
.ForMember(fl => fl.SyntaxIds,
o => o.MapFrom(fl =>
fl.FilterListSyntaxes.Select(fls => fls.SyntaxId).OrderBy(sid => sid).AsEnumerable()))
.ForMember(fl => fl.Iso6391s,
o => o.MapFrom(fl =>
fl.FilterListLanguages.Select(fls => fls.Iso6391).OrderBy(i => i).AsEnumerable()))
.ForMember(fl => fl.TagIds,
o => o.MapFrom(fl =>
fl.FilterListTags.Select(flt => flt.TagId).OrderBy(tid => tid).AsEnumerable()))
.ForMember(fl => fl.PrimaryViewUrl,
o => o.MapFrom(fl =>
fl.ViewUrls.OrderBy(u => u.SegmentNumber).ThenBy(u => u.Primariness).Select(u => u.Url)
.FirstOrDefault()))
.ForMember(fl => fl.MaintainerIds,
o => o.MapFrom(fl =>
fl.FilterListMaintainers.Select(flm => flm.MaintainerId).OrderBy(mid => mid)
.AsEnumerable()));
}
}
}

View file

@ -5,56 +5,55 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetMaintainers
{
public static class GetMaintainers
public class Query : IRequest<IEnumerable<MaintainerVm>>
{
public class Query : IRequest<IEnumerable<MaintainerVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<MaintainerVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<MaintainerVm>>
public async Task<IEnumerable<MaintainerVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<MaintainerVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Maintainers
.OrderBy(m => m.Id)
.ProjectTo<MaintainerVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class MaintainerVmProfile : Profile
{
public MaintainerVmProfile()
{
CreateMap<Maintainer, MaintainerVm>()
.ForMember(m => m.FilterListIds,
o => o.MapFrom(m =>
m.FilterListMaintainers.Select(flm => flm.FilterListId).OrderBy(flid => flid)
.AsEnumerable()));
}
}
public class MaintainerVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public string? EmailAddress { get; init; }
public string? TwitterHandle { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
return await _context.Maintainers
.OrderBy(m => m.Id)
.ProjectTo<MaintainerVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class MaintainerVmProfile : Profile
{
public MaintainerVmProfile()
{
CreateMap<Maintainer, MaintainerVm>()
.ForMember(m => m.FilterListIds,
o => o.MapFrom(m =>
m.FilterListMaintainers.Select(flm => flm.FilterListId).OrderBy(flid => flid)
.AsEnumerable()));
}
}
public class MaintainerVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public string? EmailAddress { get; init; }
public string? TwitterHandle { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
}
}

View file

@ -5,56 +5,55 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetSoftware
{
public static class GetSoftware
public class Query : IRequest<IEnumerable<SoftwareVm>>
{
public class Query : IRequest<IEnumerable<SoftwareVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<SoftwareVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<SoftwareVm>>
public async Task<IEnumerable<SoftwareVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<SoftwareVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Software
.OrderBy(s => s.Id)
.ProjectTo<SoftwareVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class SoftwareVmProfile : Profile
{
public SoftwareVmProfile()
{
CreateMap<Software, SoftwareVm>()
.ForMember(s => s.SyntaxIds,
o => o.MapFrom(s =>
s.SoftwareSyntaxes.Select(ss => ss.SyntaxId).OrderBy(sid => sid).AsEnumerable()));
}
}
public class SoftwareVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? DownloadUrl { get; init; }
public bool SupportsAbpUrlScheme { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
return await _context.Software
.OrderBy(s => s.Id)
.ProjectTo<SoftwareVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class SoftwareVmProfile : Profile
{
public SoftwareVmProfile()
{
CreateMap<Software, SoftwareVm>()
.ForMember(s => s.SyntaxIds,
o => o.MapFrom(s =>
s.SoftwareSyntaxes.Select(ss => ss.SyntaxId).OrderBy(sid => sid).AsEnumerable()));
}
}
public class SoftwareVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? DownloadUrl { get; init; }
public bool SupportsAbpUrlScheme { get; init; }
public IEnumerable<int>? SyntaxIds { get; init; }
}
}

View file

@ -5,58 +5,57 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetSyntaxes
{
public static class GetSyntaxes
public class Query : IRequest<IEnumerable<SyntaxVm>>
{
public class Query : IRequest<IEnumerable<SyntaxVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<SyntaxVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<SyntaxVm>>
public async Task<IEnumerable<SyntaxVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<SyntaxVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Syntaxes
.OrderBy(s => s.Id)
.ProjectTo<SyntaxVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class SyntaxVmProfile : Profile
{
public SyntaxVmProfile()
{
CreateMap<Syntax, SyntaxVm>()
.ForMember(s => s.FilterListIds,
o => o.MapFrom(s =>
s.FilterListSyntaxes.Select(sls => sls.FilterListId).OrderBy(flid => flid).AsEnumerable()))
.ForMember(s => s.SoftwareIds,
o => o.MapFrom(s =>
s.SoftwareSyntaxes.Select(ss => ss.SoftwareId).OrderBy(sid => sid).AsEnumerable()));
}
}
public class SyntaxVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? Url { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
public IEnumerable<int>? SoftwareIds { get; init; }
return await _context.Syntaxes
.OrderBy(s => s.Id)
.ProjectTo<SyntaxVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class SyntaxVmProfile : Profile
{
public SyntaxVmProfile()
{
CreateMap<Syntax, SyntaxVm>()
.ForMember(s => s.FilterListIds,
o => o.MapFrom(s =>
s.FilterListSyntaxes.Select(sls => sls.FilterListId).OrderBy(flid => flid).AsEnumerable()))
.ForMember(s => s.SoftwareIds,
o => o.MapFrom(s =>
s.SoftwareSyntaxes.Select(ss => ss.SoftwareId).OrderBy(sid => sid).AsEnumerable()));
}
}
public class SyntaxVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? Url { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
public IEnumerable<int>? SoftwareIds { get; init; }
}
}

View file

@ -5,53 +5,52 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Application.Queries
namespace FilterLists.Directory.Application.Queries;
public static class GetTags
{
public static class GetTags
public class Query : IRequest<IEnumerable<TagVm>>
{
public class Query : IRequest<IEnumerable<TagVm>>
}
internal class Handler : IRequestHandler<Query, IEnumerable<TagVm>>
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
internal class Handler : IRequestHandler<Query, IEnumerable<TagVm>>
public async Task<IEnumerable<TagVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
private readonly IQueryContext _context;
private readonly IMapper _mapper;
public Handler(IQueryContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<IEnumerable<TagVm>> Handle(
Query request,
CancellationToken cancellationToken)
{
return await _context.Tags
.OrderBy(t => t.Id)
.ProjectTo<TagVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class TagVmProfile : Profile
{
public TagVmProfile()
{
CreateMap<Tag, TagVm>()
.ForMember(t => t.FilterListIds,
o => o.MapFrom(t =>
t.FilterListTags.Select(flt => flt.FilterListId).OrderBy(flid => flid).AsEnumerable()));
}
}
public class TagVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
return await _context.Tags
.OrderBy(t => t.Id)
.ProjectTo<TagVm>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);
}
}
internal class TagVmProfile : Profile
{
public TagVmProfile()
{
CreateMap<Tag, TagVm>()
.ForMember(t => t.FilterListIds,
o => o.MapFrom(t =>
t.FilterListTags.Select(flt => flt.FilterListId).OrderBy(flid => flid).AsEnumerable()));
}
}
public class TagVm
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public IEnumerable<int>? FilterListIds { get; init; }
}
}

View file

@ -3,25 +3,24 @@
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace FilterLists.Directory.Infrastructure.Migrations.Tests
namespace FilterLists.Directory.Infrastructure.Migrations.Tests;
public class SeedQueryDbContextTest
{
public class SeedQueryDbContextTest
[Fact]
public async Task Migrate_DoesNotThrowException()
{
[Fact]
public async Task Migrate_DoesNotThrowException()
var exception = await Record.ExceptionAsync(async () =>
{
var exception = await Record.ExceptionAsync(async () =>
{
var connString = Environment.GetEnvironmentVariable("ConnectionStrings__DirectoryConnection") ??
throw new Exception();
var options = new DbContextOptionsBuilder<QueryDbContext>()
.UseNpgsql(connString, m => m.MigrationsAssembly(typeof(Initial).Assembly.GetName().Name))
.EnableSensitiveDataLogging()
.Options;
await using var context = new QueryDbContext(options);
await context.Database.MigrateAsync();
});
Assert.Null(exception);
}
var connString = Environment.GetEnvironmentVariable("ConnectionStrings__DirectoryConnection") ??
throw new Exception();
var options = new DbContextOptionsBuilder<QueryDbContext>()
.UseNpgsql(connString, m => m.MigrationsAssembly(typeof(Initial).Assembly.GetName().Name))
.EnableSensitiveDataLogging()
.Options;
await using var context = new QueryDbContext(options);
await context.Database.MigrateAsync();
});
Assert.Null(exception);
}
}

View file

@ -6,29 +6,28 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Directory.Infrastructure
namespace FilterLists.Directory.Infrastructure;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static IHostBuilder UseInfrastructure(this IHostBuilder hostBuilder)
{
public static IHostBuilder UseInfrastructure(this IHostBuilder hostBuilder)
{
return hostBuilder.UseLogging();
}
return hostBuilder.UseLogging();
}
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddSharedKernelLogging(configuration);
services.AddDbContextPool<QueryDbContext>(o =>
{
services.AddSharedKernelLogging(configuration);
services.AddDbContextPool<QueryDbContext>(o =>
{
o.UseNpgsql(configuration.GetConnectionString("DirectoryConnection"),
po => po.MigrationsAssembly("FilterLists.Directory.Infrastructure.Migrations"));
});
services.AddScoped<IQueryContext, QueryContext>();
}
o.UseNpgsql(configuration.GetConnectionString("DirectoryConnection"),
po => po.MigrationsAssembly("FilterLists.Directory.Infrastructure.Migrations"));
});
services.AddScoped<IQueryContext, QueryContext>();
}
public static void UseInfrastructure(this IApplicationBuilder app)
{
app.UseLogging();
}
public static void UseInfrastructure(this IApplicationBuilder app)
{
app.UseLogging();
}
}

View file

@ -1,15 +1,14 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
public interface IQueryContext
{
public interface IQueryContext
{
IQueryable<FilterList> FilterLists { get; }
IQueryable<Language> Languages { get; }
IQueryable<License> Licenses { get; }
IQueryable<Maintainer> Maintainers { get; }
IQueryable<Software> Software { get; }
IQueryable<Syntax> Syntaxes { get; }
IQueryable<Tag> Tags { get; }
}
IQueryable<FilterList> FilterLists { get; }
IQueryable<Language> Languages { get; }
IQueryable<License> Licenses { get; }
IQueryable<Maintainer> Maintainers { get; }
IQueryable<Software> Software { get; }
IQueryable<Syntax> Syntaxes { get; }
IQueryable<Tag> Tags { get; }
}

View file

@ -1,28 +1,27 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
internal class QueryContext : IQueryContext, IAsyncDisposable
{
internal class QueryContext : IQueryContext, IAsyncDisposable
private readonly QueryDbContext _dbContext;
public QueryContext(QueryDbContext dbContext)
{
private readonly QueryDbContext _dbContext;
public QueryContext(QueryDbContext dbContext)
{
_dbContext = dbContext;
}
public async ValueTask DisposeAsync()
{
await _dbContext.DisposeAsync();
}
public IQueryable<FilterList> FilterLists => _dbContext.FilterLists.AsNoTracking();
public IQueryable<Language> Languages => _dbContext.Languages.AsNoTracking();
public IQueryable<License> Licenses => _dbContext.Licenses.AsNoTracking();
public IQueryable<Maintainer> Maintainers => _dbContext.Maintainers.AsNoTracking();
public IQueryable<Software> Software => _dbContext.Software.AsNoTracking();
public IQueryable<Syntax> Syntaxes => _dbContext.Syntaxes.AsNoTracking();
public IQueryable<Tag> Tags => _dbContext.Tags.AsNoTracking();
_dbContext = dbContext;
}
public async ValueTask DisposeAsync()
{
await _dbContext.DisposeAsync();
}
public IQueryable<FilterList> FilterLists => _dbContext.FilterLists.AsNoTracking();
public IQueryable<Language> Languages => _dbContext.Languages.AsNoTracking();
public IQueryable<License> Licenses => _dbContext.Licenses.AsNoTracking();
public IQueryable<Maintainer> Maintainers => _dbContext.Maintainers.AsNoTracking();
public IQueryable<Software> Software => _dbContext.Software.AsNoTracking();
public IQueryable<Syntax> Syntaxes => _dbContext.Syntaxes.AsNoTracking();
public IQueryable<Tag> Tags => _dbContext.Tags.AsNoTracking();
}

View file

@ -1,37 +1,36 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
public class QueryDbContext : DbContext
{
public class QueryDbContext : DbContext
public QueryDbContext(DbContextOptions<QueryDbContext> options) : base(options)
{
public QueryDbContext(DbContextOptions<QueryDbContext> options) : base(options)
{
}
}
public DbSet<FilterList> FilterLists => Set<FilterList>();
public DbSet<Language> Languages => Set<Language>();
public DbSet<License> Licenses => Set<License>();
public DbSet<Maintainer> Maintainers => Set<Maintainer>();
public DbSet<Software> Software => Set<Software>();
public DbSet<Syntax> Syntaxes => Set<Syntax>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<FilterList> FilterLists => Set<FilterList>();
public DbSet<Language> Languages => Set<Language>();
public DbSet<License> Licenses => Set<License>();
public DbSet<Maintainer> Maintainers => Set<Maintainer>();
public DbSet<Software> Software => Set<Software>();
public DbSet<Syntax> Syntaxes => Set<Syntax>();
public DbSet<Tag> Tags => Set<Tag>();
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
throw new InvalidOperationException("This context is read-only.");
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
throw new InvalidOperationException("This context is read-only.");
}
public override Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
throw new InvalidOperationException("This context is read-only.");
}
public override Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
throw new InvalidOperationException("This context is read-only.");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
}
}

View file

@ -1,29 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Dependent
{
public int DependencyFilterListId { get; init; }
public FilterList DependencyFilterList { get; } = null!;
public int DependentFilterListId { get; init; }
public FilterList DependentFilterList { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class DependentTypeConfiguration : IEntityTypeConfiguration<Dependent>
public class Dependent
{
public int DependencyFilterListId { get; init; }
public FilterList DependencyFilterList { get; } = null!;
public int DependentFilterListId { get; init; }
public FilterList DependentFilterList { get; } = null!;
}
internal class DependentTypeConfiguration : IEntityTypeConfiguration<Dependent>
{
public virtual void Configure(EntityTypeBuilder<Dependent> builder)
{
public virtual void Configure(EntityTypeBuilder<Dependent> builder)
{
builder.ToTable(nameof(Dependent) + "s");
builder.HasKey(d => new { d.DependencyFilterListId, d.DependentFilterListId });
builder.HasOne(d => d.DependencyFilterList)
.WithMany(fl => fl.DependentFilterLists)
.HasForeignKey(d => d.DependencyFilterListId);
builder.HasOne(d => d.DependentFilterList)
.WithMany(fl => fl.DependencyFilterLists)
.HasForeignKey(d => d.DependentFilterListId);
builder.HasDataJsonFile<Dependent>();
}
builder.ToTable(nameof(Dependent) + "s");
builder.HasKey(d => new { d.DependencyFilterListId, d.DependentFilterListId });
builder.HasOne(d => d.DependencyFilterList)
.WithMany(fl => fl.DependentFilterLists)
.HasForeignKey(d => d.DependencyFilterListId);
builder.HasOne(d => d.DependentFilterList)
.WithMany(fl => fl.DependencyFilterLists)
.HasForeignKey(d => d.DependentFilterListId);
builder.HasDataJsonFile<Dependent>();
}
}

View file

@ -1,42 +1,41 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterList
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public License? License { get; }
public IReadOnlyCollection<FilterListSyntax> FilterListSyntaxes { get; } = new HashSet<FilterListSyntax>();
public IReadOnlyCollection<FilterListLanguage> FilterListLanguages { get; } = new HashSet<FilterListLanguage>();
public IReadOnlyCollection<FilterListTag> FilterListTags { get; } = new HashSet<FilterListTag>();
public IReadOnlyCollection<FilterListViewUrl> ViewUrls { get; } = new HashSet<FilterListViewUrl>();
public Uri? HomeUrl { get; init; }
public Uri? OnionUrl { get; init; }
public Uri? PolicyUrl { get; init; }
public Uri? SubmissionUrl { get; init; }
public Uri? IssuesUrl { get; init; }
public Uri? ForumUrl { get; init; }
public Uri? ChatUrl { get; init; }
public string? EmailAddress { get; init; }
public Uri? DonateUrl { get; init; }
public IReadOnlyCollection<FilterListMaintainer> FilterListMaintainers { get; } = new HashSet<FilterListMaintainer>();
public IReadOnlyCollection<Fork> UpstreamFilterLists { get; } = new HashSet<Fork>();
public IReadOnlyCollection<Fork> ForkFilterLists { get; } = new HashSet<Fork>();
public IReadOnlyCollection<Merge> IncludedInFilterLists { get; } = new HashSet<Merge>();
public IReadOnlyCollection<Merge> IncludesFilterLists { get; } = new HashSet<Merge>();
public IReadOnlyCollection<Dependent> DependencyFilterLists { get; } = new HashSet<Dependent>();
public IReadOnlyCollection<Dependent> DependentFilterLists { get; } = new HashSet<Dependent>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListTypeConfiguration : IEntityTypeConfiguration<FilterList>
public class FilterList
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public int? LicenseId { get; init; }
public License? License { get; }
public IReadOnlyCollection<FilterListSyntax> FilterListSyntaxes { get; } = new HashSet<FilterListSyntax>();
public IReadOnlyCollection<FilterListLanguage> FilterListLanguages { get; } = new HashSet<FilterListLanguage>();
public IReadOnlyCollection<FilterListTag> FilterListTags { get; } = new HashSet<FilterListTag>();
public IReadOnlyCollection<FilterListViewUrl> ViewUrls { get; } = new HashSet<FilterListViewUrl>();
public Uri? HomeUrl { get; init; }
public Uri? OnionUrl { get; init; }
public Uri? PolicyUrl { get; init; }
public Uri? SubmissionUrl { get; init; }
public Uri? IssuesUrl { get; init; }
public Uri? ForumUrl { get; init; }
public Uri? ChatUrl { get; init; }
public string? EmailAddress { get; init; }
public Uri? DonateUrl { get; init; }
public IReadOnlyCollection<FilterListMaintainer> FilterListMaintainers { get; } = new HashSet<FilterListMaintainer>();
public IReadOnlyCollection<Fork> UpstreamFilterLists { get; } = new HashSet<Fork>();
public IReadOnlyCollection<Fork> ForkFilterLists { get; } = new HashSet<Fork>();
public IReadOnlyCollection<Merge> IncludedInFilterLists { get; } = new HashSet<Merge>();
public IReadOnlyCollection<Merge> IncludesFilterLists { get; } = new HashSet<Merge>();
public IReadOnlyCollection<Dependent> DependencyFilterLists { get; } = new HashSet<Dependent>();
public IReadOnlyCollection<Dependent> DependentFilterLists { get; } = new HashSet<Dependent>();
}
internal class FilterListTypeConfiguration : IEntityTypeConfiguration<FilterList>
{
public virtual void Configure(EntityTypeBuilder<FilterList> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterList> builder)
{
builder.HasDataJsonFile<FilterList>();
}
builder.HasDataJsonFile<FilterList>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterListLanguage
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public string Iso6391 { get; init; } = null!;
public Language Language { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListLanguageTypeConfiguration : IEntityTypeConfiguration<FilterListLanguage>
public class FilterListLanguage
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public string Iso6391 { get; init; } = null!;
public Language Language { get; } = null!;
}
internal class FilterListLanguageTypeConfiguration : IEntityTypeConfiguration<FilterListLanguage>
{
public virtual void Configure(EntityTypeBuilder<FilterListLanguage> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterListLanguage> builder)
{
builder.ToTable(nameof(FilterListLanguage) + "s");
builder.HasKey(fll => new { fll.FilterListId, fll.Iso6391 });
builder.HasDataJsonFile<FilterListLanguage>();
}
builder.ToTable(nameof(FilterListLanguage) + "s");
builder.HasKey(fll => new { fll.FilterListId, fll.Iso6391 });
builder.HasDataJsonFile<FilterListLanguage>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterListMaintainer
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int MaintainerId { get; init; }
public Maintainer Maintainer { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListMaintainerTypeConfiguration : IEntityTypeConfiguration<FilterListMaintainer>
public class FilterListMaintainer
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int MaintainerId { get; init; }
public Maintainer Maintainer { get; } = null!;
}
internal class FilterListMaintainerTypeConfiguration : IEntityTypeConfiguration<FilterListMaintainer>
{
public virtual void Configure(EntityTypeBuilder<FilterListMaintainer> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterListMaintainer> builder)
{
builder.ToTable(nameof(FilterListMaintainer) + "s");
builder.HasKey(flm => new { flm.FilterListId, flm.MaintainerId });
builder.HasDataJsonFile<FilterListMaintainer>();
}
builder.ToTable(nameof(FilterListMaintainer) + "s");
builder.HasKey(flm => new { flm.FilterListId, flm.MaintainerId });
builder.HasDataJsonFile<FilterListMaintainer>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterListSyntax
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int SyntaxId { get; init; }
public Syntax Syntax { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListSyntaxTypeConfiguration : IEntityTypeConfiguration<FilterListSyntax>
public class FilterListSyntax
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int SyntaxId { get; init; }
public Syntax Syntax { get; } = null!;
}
internal class FilterListSyntaxTypeConfiguration : IEntityTypeConfiguration<FilterListSyntax>
{
public virtual void Configure(EntityTypeBuilder<FilterListSyntax> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterListSyntax> builder)
{
builder.ToTable(nameof(FilterListSyntax) + "es");
builder.HasKey(fls => new { fls.FilterListId, fls.SyntaxId });
builder.HasDataJsonFile<FilterListSyntax>();
}
builder.ToTable(nameof(FilterListSyntax) + "es");
builder.HasKey(fls => new { fls.FilterListId, fls.SyntaxId });
builder.HasDataJsonFile<FilterListSyntax>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterListTag
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int TagId { get; init; }
public Tag Tag { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListTagTypeConfiguration : IEntityTypeConfiguration<FilterListTag>
public class FilterListTag
{
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public int TagId { get; init; }
public Tag Tag { get; } = null!;
}
internal class FilterListTagTypeConfiguration : IEntityTypeConfiguration<FilterListTag>
{
public virtual void Configure(EntityTypeBuilder<FilterListTag> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterListTag> builder)
{
builder.ToTable(nameof(FilterListTag) + "s");
builder.HasKey(flt => new { flt.FilterListId, flt.TagId });
builder.HasDataJsonFile<FilterListTag>();
}
builder.ToTable(nameof(FilterListTag) + "s");
builder.HasKey(flt => new { flt.FilterListId, flt.TagId });
builder.HasDataJsonFile<FilterListTag>();
}
}

View file

@ -1,27 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class FilterListViewUrl
{
public int Id { get; init; }
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public short SegmentNumber { get; init; }
public short Primariness { get; init; }
public Uri Url { get; init; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class FilterListViewUrlConfiguration : IEntityTypeConfiguration<FilterListViewUrl>
public class FilterListViewUrl
{
public int Id { get; init; }
public int FilterListId { get; init; }
public FilterList FilterList { get; } = null!;
public short SegmentNumber { get; init; }
public short Primariness { get; init; }
public Uri Url { get; init; } = null!;
}
internal class FilterListViewUrlConfiguration : IEntityTypeConfiguration<FilterListViewUrl>
{
public virtual void Configure(EntityTypeBuilder<FilterListViewUrl> builder)
{
public virtual void Configure(EntityTypeBuilder<FilterListViewUrl> builder)
{
builder.ToTable(nameof(FilterListViewUrl) + "s");
builder.Property(u => u.SegmentNumber).HasDefaultValue(1);
builder.Property(u => u.Primariness).HasDefaultValue(1);
builder.HasIndex(u => new { u.FilterListId, u.SegmentNumber, u.Primariness }).IsUnique();
builder.HasDataJsonFile<FilterListViewUrl>();
}
builder.ToTable(nameof(FilterListViewUrl) + "s");
builder.Property(u => u.SegmentNumber).HasDefaultValue(1);
builder.Property(u => u.Primariness).HasDefaultValue(1);
builder.HasIndex(u => new { u.FilterListId, u.SegmentNumber, u.Primariness }).IsUnique();
builder.HasDataJsonFile<FilterListViewUrl>();
}
}

View file

@ -1,29 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Fork
{
public int UpstreamFilterListId { get; init; }
public FilterList UpstreamFilterList { get; } = null!;
public int ForkFilterListId { get; init; }
public FilterList ForkFilterList { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class ForkTypeConfiguration : IEntityTypeConfiguration<Fork>
public class Fork
{
public int UpstreamFilterListId { get; init; }
public FilterList UpstreamFilterList { get; } = null!;
public int ForkFilterListId { get; init; }
public FilterList ForkFilterList { get; } = null!;
}
internal class ForkTypeConfiguration : IEntityTypeConfiguration<Fork>
{
public virtual void Configure(EntityTypeBuilder<Fork> builder)
{
public virtual void Configure(EntityTypeBuilder<Fork> builder)
{
builder.ToTable(nameof(Fork) + "s");
builder.HasKey(f => new { f.UpstreamFilterListId, f.ForkFilterListId });
builder.HasOne(f => f.UpstreamFilterList)
.WithMany(fl => fl.ForkFilterLists)
.HasForeignKey(f => f.UpstreamFilterListId);
builder.HasOne(f => f.ForkFilterList)
.WithMany(fl => fl.UpstreamFilterLists)
.HasForeignKey(f => f.ForkFilterListId);
builder.HasDataJsonFile<Fork>();
}
builder.ToTable(nameof(Fork) + "s");
builder.HasKey(f => new { f.UpstreamFilterListId, f.ForkFilterListId });
builder.HasOne(f => f.UpstreamFilterList)
.WithMany(fl => fl.ForkFilterLists)
.HasForeignKey(f => f.UpstreamFilterListId);
builder.HasOne(f => f.ForkFilterList)
.WithMany(fl => fl.UpstreamFilterLists)
.HasForeignKey(f => f.ForkFilterListId);
builder.HasDataJsonFile<Fork>();
}
}

View file

@ -1,24 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Language
{
public string Iso6391 { get; init; } = null!;
public string Name { get; init; } = null!;
public IReadOnlyCollection<FilterListLanguage> FilterListLanguages { get; } = new HashSet<FilterListLanguage>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class LanguageTypeConfiguration : IEntityTypeConfiguration<Language>
public class Language
{
public string Iso6391 { get; init; } = null!;
public string Name { get; init; } = null!;
public IReadOnlyCollection<FilterListLanguage> FilterListLanguages { get; } = new HashSet<FilterListLanguage>();
}
internal class LanguageTypeConfiguration : IEntityTypeConfiguration<Language>
{
public virtual void Configure(EntityTypeBuilder<Language> builder)
{
public virtual void Configure(EntityTypeBuilder<Language> builder)
{
builder.HasKey(l => l.Iso6391);
builder.Property(l => l.Iso6391)
.IsFixedLength()
.HasMaxLength(2);
builder.HasDataJsonFile<Language>();
}
builder.HasKey(l => l.Iso6391);
builder.Property(l => l.Iso6391)
.IsFixedLength()
.HasMaxLength(2);
builder.HasDataJsonFile<Language>();
}
}

View file

@ -1,24 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class License
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public bool PermitsModification { get; init; }
public bool PermitsDistribution { get; init; }
public bool PermitsCommercialUse { get; init; }
public IReadOnlyCollection<FilterList> FilterLists { get; } = new HashSet<FilterList>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class LicenseTypeConfiguration : IEntityTypeConfiguration<License>
public class License
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public bool PermitsModification { get; init; }
public bool PermitsDistribution { get; init; }
public bool PermitsCommercialUse { get; init; }
public IReadOnlyCollection<FilterList> FilterLists { get; } = new HashSet<FilterList>();
}
internal class LicenseTypeConfiguration : IEntityTypeConfiguration<License>
{
public virtual void Configure(EntityTypeBuilder<License> builder)
{
public virtual void Configure(EntityTypeBuilder<License> builder)
{
builder.HasDataJsonFile<License>();
}
builder.HasDataJsonFile<License>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Maintainer
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public string? EmailAddress { get; init; }
public string? TwitterHandle { get; init; }
public IReadOnlyCollection<FilterListMaintainer> FilterListMaintainers { get; } = new HashSet<FilterListMaintainer>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class MaintainerTypeConfiguration : IEntityTypeConfiguration<Maintainer>
public class Maintainer
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public Uri? Url { get; init; }
public string? EmailAddress { get; init; }
public string? TwitterHandle { get; init; }
public IReadOnlyCollection<FilterListMaintainer> FilterListMaintainers { get; } = new HashSet<FilterListMaintainer>();
}
internal class MaintainerTypeConfiguration : IEntityTypeConfiguration<Maintainer>
{
public virtual void Configure(EntityTypeBuilder<Maintainer> builder)
{
public virtual void Configure(EntityTypeBuilder<Maintainer> builder)
{
builder.HasDataJsonFile<Maintainer>();
}
builder.HasDataJsonFile<Maintainer>();
}
}

View file

@ -1,29 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Merge
{
public int IncludedInFilterListId { get; init; }
public FilterList IncludedInFilterList { get; } = null!;
public int IncludesFilterListId { get; init; }
public FilterList IncludesFilterList { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class MergeTypeConfiguration : IEntityTypeConfiguration<Merge>
public class Merge
{
public int IncludedInFilterListId { get; init; }
public FilterList IncludedInFilterList { get; } = null!;
public int IncludesFilterListId { get; init; }
public FilterList IncludesFilterList { get; } = null!;
}
internal class MergeTypeConfiguration : IEntityTypeConfiguration<Merge>
{
public virtual void Configure(EntityTypeBuilder<Merge> builder)
{
public virtual void Configure(EntityTypeBuilder<Merge> builder)
{
builder.ToTable(nameof(Merge) + "s");
builder.HasKey(m => new { m.IncludedInFilterListId, m.IncludesFilterListId });
builder.HasOne(m => m.IncludedInFilterList)
.WithMany(fl => fl.IncludesFilterLists)
.HasForeignKey(m => m.IncludedInFilterListId);
builder.HasOne(m => m.IncludesFilterList)
.WithMany(fl => fl.IncludedInFilterLists)
.HasForeignKey(m => m.IncludesFilterListId);
builder.HasDataJsonFile<Merge>();
}
builder.ToTable(nameof(Merge) + "s");
builder.HasKey(m => new { m.IncludedInFilterListId, m.IncludesFilterListId });
builder.HasOne(m => m.IncludedInFilterList)
.WithMany(fl => fl.IncludesFilterLists)
.HasForeignKey(m => m.IncludedInFilterListId);
builder.HasOne(m => m.IncludesFilterList)
.WithMany(fl => fl.IncludedInFilterLists)
.HasForeignKey(m => m.IncludesFilterListId);
builder.HasDataJsonFile<Merge>();
}
}

View file

@ -1,24 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Software
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? DownloadUrl { get; init; }
public bool SupportsAbpUrlScheme { get; init; }
public IReadOnlyCollection<SoftwareSyntax> SoftwareSyntaxes { get; } = new HashSet<SoftwareSyntax>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class SoftwareTypeConfiguration : IEntityTypeConfiguration<Software>
public class Software
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? HomeUrl { get; init; }
public Uri? DownloadUrl { get; init; }
public bool SupportsAbpUrlScheme { get; init; }
public IReadOnlyCollection<SoftwareSyntax> SoftwareSyntaxes { get; } = new HashSet<SoftwareSyntax>();
}
internal class SoftwareTypeConfiguration : IEntityTypeConfiguration<Software>
{
public virtual void Configure(EntityTypeBuilder<Software> builder)
{
public virtual void Configure(EntityTypeBuilder<Software> builder)
{
builder.HasDataJsonFile<Software>();
}
builder.HasDataJsonFile<Software>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class SoftwareSyntax
{
public int SoftwareId { get; init; }
public Software Software { get; } = null!;
public int SyntaxId { get; init; }
public Syntax Syntax { get; } = null!;
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class SoftwareSyntaxTypeConfiguration : IEntityTypeConfiguration<SoftwareSyntax>
public class SoftwareSyntax
{
public int SoftwareId { get; init; }
public Software Software { get; } = null!;
public int SyntaxId { get; init; }
public Syntax Syntax { get; } = null!;
}
internal class SoftwareSyntaxTypeConfiguration : IEntityTypeConfiguration<SoftwareSyntax>
{
public virtual void Configure(EntityTypeBuilder<SoftwareSyntax> builder)
{
public virtual void Configure(EntityTypeBuilder<SoftwareSyntax> builder)
{
builder.ToTable(nameof(SoftwareSyntax) + "es");
builder.HasKey(ss => new { ss.SoftwareId, ss.SyntaxId });
builder.HasDataJsonFile<SoftwareSyntax>();
}
builder.ToTable(nameof(SoftwareSyntax) + "es");
builder.HasKey(ss => new { ss.SoftwareId, ss.SyntaxId });
builder.HasDataJsonFile<SoftwareSyntax>();
}
}

View file

@ -1,23 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Syntax
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? Url { get; init; }
public IReadOnlyCollection<FilterListSyntax> FilterListSyntaxes { get; } = new HashSet<FilterListSyntax>();
public IReadOnlyCollection<SoftwareSyntax> SoftwareSyntaxes { get; } = new HashSet<SoftwareSyntax>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class SyntaxTypeConfiguration : IEntityTypeConfiguration<Syntax>
public class Syntax
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public Uri? Url { get; init; }
public IReadOnlyCollection<FilterListSyntax> FilterListSyntaxes { get; } = new HashSet<FilterListSyntax>();
public IReadOnlyCollection<SoftwareSyntax> SoftwareSyntaxes { get; } = new HashSet<SoftwareSyntax>();
}
internal class SyntaxTypeConfiguration : IEntityTypeConfiguration<Syntax>
{
public virtual void Configure(EntityTypeBuilder<Syntax> builder)
{
public virtual void Configure(EntityTypeBuilder<Syntax> builder)
{
builder.HasDataJsonFile<Syntax>();
}
builder.HasDataJsonFile<Syntax>();
}
}

View file

@ -1,21 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities
{
public class Tag
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public IReadOnlyCollection<FilterListTag> FilterListTags { get; } = new HashSet<FilterListTag>();
}
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
internal class TagTypeConfiguration : IEntityTypeConfiguration<Tag>
public class Tag
{
public int Id { get; init; }
public string Name { get; init; } = null!;
public string? Description { get; init; }
public IReadOnlyCollection<FilterListTag> FilterListTags { get; } = new HashSet<FilterListTag>();
}
internal class TagTypeConfiguration : IEntityTypeConfiguration<Tag>
{
public virtual void Configure(EntityTypeBuilder<Tag> builder)
{
public virtual void Configure(EntityTypeBuilder<Tag> builder)
{
builder.HasDataJsonFile<Tag>();
}
builder.HasDataJsonFile<Tag>();
}
}

View file

@ -5,36 +5,35 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace FilterLists.Directory.Infrastructure.Persistence
namespace FilterLists.Directory.Infrastructure.Persistence;
public static class SeedExtension
{
public static class SeedExtension
public static async Task MigrateAsync(this IHost host)
{
public static async Task MigrateAsync(this IHost host)
{
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<QueryDbContext>();
await db.Database.MigrateAsync();
}
using var scope = host.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<QueryDbContext>();
await db.Database.MigrateAsync();
}
}
internal static class SeedConfigurationExtension
internal static class SeedConfigurationExtension
{
public static void HasDataJsonFile<TEntity>(this EntityTypeBuilder entityTypeBuilder)
{
public static void HasDataJsonFile<TEntity>(this EntityTypeBuilder entityTypeBuilder)
var path = Path.Combine("../data", $"{typeof(TEntity).Name}.json");
if (!File.Exists(path))
{
var path = Path.Combine("../data", $"{typeof(TEntity).Name}.json");
if (!File.Exists(path))
{
return;
}
return;
}
var entitiesJson = File.ReadAllText(path);
var entities = JsonSerializer.Deserialize<IEnumerable<TEntity>>(entitiesJson,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var entitiesJson = File.ReadAllText(path);
var entities = JsonSerializer.Deserialize<IEnumerable<TEntity>>(entitiesJson,
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
if (entities != null)
{
entityTypeBuilder.HasData((IEnumerable<object>)entities);
}
if (entities != null)
{
entityTypeBuilder.HasData((IEnumerable<object>)entities);
}
}
}

View file

@ -1,16 +1,15 @@
using Serilog;
using Serilog.Events;
namespace FilterLists.SharedKernel.Logging
namespace FilterLists.SharedKernel.Logging;
internal static class ConfigurationBuilder
{
internal static class ConfigurationBuilder
{
public static readonly LoggerConfiguration BaseLoggerConfiguration =
new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console();
}
public static readonly LoggerConfiguration BaseLoggerConfiguration =
new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console();
}

View file

@ -8,31 +8,30 @@
using Microsoft.Extensions.Hosting;
using Serilog;
namespace FilterLists.SharedKernel.Logging
namespace FilterLists.SharedKernel.Logging;
public static class ConfigurationExtensions
{
public static class ConfigurationExtensions
public static IHostBuilder UseLogging(this IHostBuilder hostBuilder)
{
public static IHostBuilder UseLogging(this IHostBuilder hostBuilder)
{
return hostBuilder.UseSerilog();
}
return hostBuilder.UseSerilog();
}
public static void AddSharedKernelLogging(this IServiceCollection services, IConfiguration configuration)
public static void AddSharedKernelLogging(this IServiceCollection services, IConfiguration configuration)
{
using var serverTelemetryChannel = new ServerTelemetryChannel
{
using var serverTelemetryChannel = new ServerTelemetryChannel
{
StorageFolder = configuration.GetSection(ApplicationInsightsOptions.Key)
.Get<ApplicationInsightsOptions>()
.ServerTelemetryChannelStoragePath
};
services.AddSingleton(typeof(ITelemetryChannel), serverTelemetryChannel);
TelemetryDebugWriter.IsTracingDisabled = true;
services.AddApplicationInsightsTelemetry();
}
StorageFolder = configuration.GetSection(ApplicationInsightsOptions.Key)
.Get<ApplicationInsightsOptions>()
.ServerTelemetryChannelStoragePath
};
services.AddSingleton(typeof(ITelemetryChannel), serverTelemetryChannel);
TelemetryDebugWriter.IsTracingDisabled = true;
services.AddApplicationInsightsTelemetry();
}
public static void UseLogging(this IApplicationBuilder app)
{
app.UseSerilogRequestLogging();
}
public static void UseLogging(this IApplicationBuilder app)
{
app.UseSerilogRequestLogging();
}
}

View file

@ -3,38 +3,37 @@
using Microsoft.Extensions.Hosting;
using Serilog;
namespace FilterLists.SharedKernel.Logging
namespace FilterLists.SharedKernel.Logging;
public static class HostRunner
{
public static class HostRunner
public static async Task TryRunWithLoggingAsync(this IHost host, Func<Task>? runPreHostAsync = default)
{
public static async Task TryRunWithLoggingAsync(this IHost host, Func<Task>? runPreHostAsync = default)
Log.Logger = ConfigurationBuilder.BaseLoggerConfiguration
.WriteTo.ApplicationInsights(
host.Services.GetRequiredService<TelemetryConfiguration>(),
TelemetryConverter.Traces)
.CreateLogger();
try
{
Log.Logger = ConfigurationBuilder.BaseLoggerConfiguration
.WriteTo.ApplicationInsights(
host.Services.GetRequiredService<TelemetryConfiguration>(),
TelemetryConverter.Traces)
.CreateLogger();
if (runPreHostAsync != null)
{
Log.Information("Initializing pre-host");
await runPreHostAsync();
}
try
{
if (runPreHostAsync != null)
{
Log.Information("Initializing pre-host");
await runPreHostAsync();
}
Log.Information("Initializing host");
await host.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
throw;
}
finally
{
Log.CloseAndFlush();
}
Log.Information("Initializing host");
await host.RunAsync();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
throw;
}
finally
{
Log.CloseAndFlush();
}
}
}

View file

@ -1,9 +1,8 @@
namespace FilterLists.SharedKernel.Logging.Options
{
internal class ApplicationInsightsOptions
{
public const string Key = "ApplicationInsights";
namespace FilterLists.SharedKernel.Logging.Options;
public string ServerTelemetryChannelStoragePath { get; init; } = null!;
}
internal class ApplicationInsightsOptions
{
public const string Key = "ApplicationInsights";
public string ServerTelemetryChannelStoragePath { get; init; } = null!;
}