feat(dir): 🚧 scaffold CreateList command

This commit is contained in:
Collin M. Barrett 2021-11-03 17:41:11 -05:00
parent 94dab0d15c
commit 2c5f8363e1
20 changed files with 321 additions and 57 deletions

View file

@ -1,4 +1,5 @@
using FilterLists.Directory.Api.Contracts.Models;
using FilterLists.Directory.Application.Commands;
using FilterLists.Directory.Application.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@ -16,7 +17,7 @@ public ListsController(IMemoryCache cache, IMediator mediator) : base(cache)
}
/// <summary>
/// Gets the FilterLists..
/// Gets the FilterLists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The FilterLists.</returns>
@ -27,6 +28,18 @@ public Task<IActionResult> Get(CancellationToken cancellationToken)
return CacheGetOrCreateAsync(() => _mediator.Send(new GetLists.Query(), cancellationToken));
}
/// <summary>
/// Creates the FilterList.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[HttpPost]
[ProducesResponseType(typeof(CreateList.Response), StatusCodes.Status200OK)]
public async Task<IActionResult> Create(CreateList.Command command, CancellationToken cancellationToken)
{
return Ok(await _mediator.Send(command, cancellationToken));
}
/// <summary>
/// Gets the details of the FilterList.
/// </summary>

View file

@ -0,0 +1,78 @@
using FilterLists.Directory.Domain.Aggregates;
using FilterLists.Directory.Domain.Aggregates.Changes;
using FilterLists.Directory.Domain.Aggregates.Licenses;
using FluentValidation;
using MediatR;
namespace FilterLists.Directory.Application.Commands;
public static class CreateList
{
public record Command(string Name,
string? Description = default,
int? LicenseId = default,
Uri? HomeUrl = default,
Uri? OnionUrl = default,
Uri? PolicyUrl = default,
Uri? SubmissionUrl = default,
Uri? IssuesUrl = default,
Uri? ForumUrl = default,
Uri? ChatUrl = default,
string? EmailAddress = default,
Uri? DonateUrl = default) : IRequest<Response>;
internal class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(c => c.LicenseId)
.GreaterThanOrEqualTo(0)
.When(c => c.LicenseId != null);
}
}
internal class Handler : IRequestHandler<Command, Response>
{
private readonly IChangeRepository _changeRepo;
private readonly ILicenseRepository _licenseRepo;
public Handler(IChangeRepository changeRepo, ILicenseRepository licenseRepo)
{
_changeRepo = changeRepo;
_licenseRepo = licenseRepo;
}
public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
{
var license = request.LicenseId != null
? await _licenseRepo.GetByIdAsync(request.LicenseId.Value, cancellationToken) ??
throw new ArgumentException($"LicenseId {request.LicenseId} not found.",
nameof(request.LicenseId))
: null;
var filterList = new FilterList(request.Name,
request.Description,
license,
request.HomeUrl,
request.OnionUrl,
request.PolicyUrl,
request.SubmissionUrl,
request.IssuesUrl,
request.ForumUrl,
request.ChatUrl,
request.EmailAddress,
request.DonateUrl);
//Change change = null;
//await _changeRepo.AddAsync(change, cancellationToken);
//return new Response();
throw new NotImplementedException();
}
}
public record Response
{
}
}

View file

@ -0,0 +1,12 @@
namespace FilterLists.Directory.Domain.Aggregates.Changes;
public enum AggregateRoot
{
FilterList,
Language,
License,
Maintainer,
Software,
Syntax,
Tag
}

View file

@ -0,0 +1,27 @@
using System.Text.Json;
namespace FilterLists.Directory.Domain.Aggregates.Changes;
public class Change
{
private Change()
{
}
public Change(ChangeType type, AggregateRoot aggregateRoot, int aggregateRootId, JsonDocument? aggregate)
{
Type = type;
AggregateRoot = aggregateRoot;
AggregateRootId = aggregateRootId;
Aggregate = aggregate;
}
public ChangeType Type { get; private init; }
public AggregateRoot AggregateRoot { get; private init; }
public int AggregateRootId { get; private init; }
public JsonDocument? Aggregate { get; private init; }
public DateTime CreatedAt { get; private init; }
public DateTime? AppliedAt { get; private init; }
public DateTime? RejectedAt { get; private init; }
public string? RejectedReason { get; private init; }
}

View file

@ -0,0 +1,8 @@
namespace FilterLists.Directory.Domain.Aggregates.Changes;
public enum ChangeType
{
Insert,
Update,
Delete
}

View file

@ -0,0 +1,6 @@
namespace FilterLists.Directory.Domain.Aggregates.Changes;
public interface IChangeRepository
{
Task AddAsync(Change change, CancellationToken cancellationToken);
}

View file

@ -1,18 +1,50 @@
namespace FilterLists.Directory.Domain.Aggregates;
using FilterLists.Directory.Domain.Aggregates.Licenses;
namespace FilterLists.Directory.Domain.Aggregates;
public class FilterList
{
public int Id { get; private set; }
public string Name { get; private set; } = null!;
public string? Description { get; private set; }
public int? LicenseId { get; private set; }
public Uri? HomeUrl { get; private set; }
public Uri? OnionUrl { get; private set; }
public Uri? PolicyUrl { get; private set; }
public Uri? SubmissionUrl { get; private set; }
public Uri? IssuesUrl { get; private set; }
public Uri? ForumUrl { get; private set; }
public Uri? ChatUrl { get; private set; }
public string? EmailAddress { get; private set; }
public Uri? DonateUrl { get; private set; }
private FilterList()
{
}
public FilterList(string name,
string? description,
License? license,
Uri? homeUrl,
Uri? onionUrl,
Uri? policyUrl,
Uri? submissionUrl,
Uri? issuesUrl,
Uri? forumUrl,
Uri? chatUrl,
string? emailAddress,
Uri? donateUrl)
{
Name = name;
Description = description;
License = license;
HomeUrl = homeUrl;
OnionUrl = onionUrl;
PolicyUrl = policyUrl;
SubmissionUrl = submissionUrl;
IssuesUrl = issuesUrl;
ForumUrl = forumUrl;
ChatUrl = chatUrl;
EmailAddress = emailAddress;
DonateUrl = donateUrl;
}
public string Name { get; private init; } = null!;
public string? Description { get; private init; }
public License? License { get; private init; }
public Uri? HomeUrl { get; private init; }
public Uri? OnionUrl { get; private init; }
public Uri? PolicyUrl { get; private init; }
public Uri? SubmissionUrl { get; private init; }
public Uri? IssuesUrl { get; private init; }
public Uri? ForumUrl { get; private init; }
public Uri? ChatUrl { get; private init; }
public string? EmailAddress { get; private init; }
public Uri? DonateUrl { get; private init; }
}

View file

@ -0,0 +1,6 @@
namespace FilterLists.Directory.Domain.Aggregates.Licenses;
public interface ILicenseRepository
{
ValueTask<License?> GetByIdAsync(int id, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,27 @@
namespace FilterLists.Directory.Domain.Aggregates.Licenses;
public class License
{
private License()
{
}
public License(string name,
Uri? url,
bool? permitsModification,
bool? permitsDistribution,
bool? permitsCommercialUse)
{
Name = name;
Url = url;
PermitsModification = permitsModification ?? false;
PermitsDistribution = permitsDistribution ?? false;
PermitsCommercialUse = permitsCommercialUse ?? false;
}
public string Name { get; private init; } = null!;
public Uri? Url { get; private init; }
public bool PermitsModification { get; private init; }
public bool PermitsDistribution { get; private init; }
public bool PermitsCommercialUse { get; private init; }
}

View file

@ -1,4 +1,5 @@
using FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
using FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
using FilterLists.SharedKernel.Logging;
using Microsoft.AspNetCore.Builder;
@ -32,10 +33,24 @@ public static void AddInfrastructure(this IServiceCollection services, IConfigur
o.EnableSensitiveDataLogging();
#else
;
#endif
});
services.AddDbContextPool<CommandDbContext>(o =>
{
o.UseNpgsql(configuration.GetConnectionString("DirectoryConnection"))
#if DEBUG
.LogTo(Console.WriteLine, LogLevel.Information);
o.EnableSensitiveDataLogging();
#else
;
#endif
});
services.AddScoped<IQueryContext, QueryContext>();
services.AddScoped<ICommandContext, CommandContext>();
services.AddScoped<ICommandContext, CommandDbContext>();
services.Scan(s => s.FromAssembliesOf(typeof(ConfigurationExtensions))
.AddClasses(f => f.InNamespaceOf<ChangeRepository>(), false)
.AsImplementedInterfaces()
.WithScopedLifetime());
}
public static void UseInfrastructure(this IApplicationBuilder app)

View file

@ -20,6 +20,7 @@
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0" />
<PackageReference Include="Scrutor" Version="3.3.0" />
</ItemGroup>
<ItemGroup>

View file

@ -1,16 +0,0 @@
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
internal class CommandContext : ICommandContext, IAsyncDisposable
{
private readonly CommandDbContext _dbContext;
public CommandContext(CommandDbContext dbContext)
{
_dbContext = dbContext;
}
public async ValueTask DisposeAsync()
{
await _dbContext.DisposeAsync();
}
}

View file

@ -1,15 +1,22 @@
using Microsoft.EntityFrameworkCore;
using FilterLists.Directory.Domain.Aggregates;
using FilterLists.Directory.Domain.Aggregates.Licenses;
using FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations;
using Microsoft.EntityFrameworkCore;
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
public class CommandDbContext : DbContext
internal class CommandDbContext : DbContext, ICommandContext
{
public CommandDbContext(DbContextOptions<CommandDbContext> options) : base(options)
{
}
public DbSet<FilterList> FilterLists => Set<FilterList>();
public DbSet<License> Licenses => Set<License>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly,
type => type.Namespace == typeof(FilterListTypeConfiguration).Namespace);
}
}

View file

@ -1,5 +1,11 @@
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
using FilterLists.Directory.Domain.Aggregates;
using FilterLists.Directory.Domain.Aggregates.Licenses;
using Microsoft.EntityFrameworkCore;
public interface ICommandContext
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
internal interface ICommandContext
{
DbSet<FilterList> FilterLists { get; }
DbSet<License> Licenses { get; }
}

View file

@ -0,0 +1,13 @@
using FilterLists.Directory.Domain.Aggregates;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations;
internal class FilterListTypeConfiguration : IEntityTypeConfiguration<FilterList>
{
public virtual void Configure(EntityTypeBuilder<FilterList> builder)
{
builder.Property<int>(nameof(Queries.Entities.FilterList.Id));
}
}

View file

@ -0,0 +1,13 @@
using FilterLists.Directory.Domain.Aggregates.Licenses;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations;
internal class LicenseTypeConfiguration : IEntityTypeConfiguration<License>
{
public virtual void Configure(EntityTypeBuilder<License> builder)
{
builder.Property<int>(nameof(Queries.Entities.License.Id));
}
}

View file

@ -0,0 +1,11 @@
using FilterLists.Directory.Domain.Aggregates.Changes;
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories;
internal class ChangeRepository : IChangeRepository
{
public Task AddAsync(Change change, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,19 @@
using FilterLists.Directory.Domain.Aggregates.Licenses;
using FilterLists.Directory.Infrastructure.Persistence.Commands.Context;
namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories;
internal class LicenseRepository : ILicenseRepository
{
private readonly ICommandContext _context;
public LicenseRepository(ICommandContext context)
{
_context = context;
}
public ValueTask<License?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
return _context.Licenses.FindAsync(new object[] { id }, cancellationToken);
}
}

View file

@ -1,6 +1,8 @@
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using FilterLists.Directory.Domain.Aggregates.Changes;
using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Change = FilterLists.Directory.Infrastructure.Persistence.Queries.Entities.Change;
namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context;
@ -39,7 +41,8 @@ public override Task<int> SaveChangesAsync(
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly,
type => type.Namespace == typeof(FilterListTypeConfiguration).Namespace);
modelBuilder.HasPostgresEnum<ChangeType>();
modelBuilder.HasPostgresEnum<AggregateRoot>();
}

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using FilterLists.Directory.Domain.Aggregates.Changes;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -17,24 +18,6 @@ public class Change
public string? RejectedReason { get; init; }
}
public enum ChangeType
{
Insert,
Update,
Delete
}
public enum AggregateRoot
{
FilterList,
Language,
License,
Maintainer,
Software,
Syntax,
Tag
}
internal class UpdateConfiguration : IEntityTypeConfiguration<Change>
{
public virtual void Configure(EntityTypeBuilder<Change> builder)