diff --git a/Dfe.Academies.Api.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/Dfe.Academies.Api.Infrastructure/InfrastructureServiceCollectionExtensions.cs index 59d9b79d4..05363a197 100644 --- a/Dfe.Academies.Api.Infrastructure/InfrastructureServiceCollectionExtensions.cs +++ b/Dfe.Academies.Api.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Dfe.Academies.Application.Common.Interfaces; +using Dfe.Academies.Domain.Constituencies; using Dfe.Academies.Infrastructure; using Dfe.Academies.Infrastructure.Caching; using Dfe.Academies.Infrastructure.Repositories; @@ -8,6 +9,7 @@ using Dfe.Academies.Domain.Interfaces.Repositories; using Dfe.Academies.Domain.Interfaces.Caching; using Dfe.Academies.Infrastructure.QueryServices; +using Dfe.Academies.Domain.ValueObjects; namespace Microsoft.Extensions.DependencyInjection { @@ -41,8 +43,8 @@ public static IServiceCollection AddPersonsApiInfrastructureDependencyGroup( services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(typeof(IMstrRepository<>), typeof(MstrRepository<>)); - services.AddScoped(typeof(IMopRepository<>), typeof(MopRepository<>)); + services.AddScoped(typeof(IMstrRepository<,>), typeof(MstrRepository<,>)); + services.AddScoped(typeof(IMopRepository<,>), typeof(MopRepository<,>)); // Query Services services.AddScoped(); diff --git a/Dfe.Academies.Api.Infrastructure/MopContext.cs b/Dfe.Academies.Api.Infrastructure/MopContext.cs index 097341527..861c4fadc 100644 --- a/Dfe.Academies.Api.Infrastructure/MopContext.cs +++ b/Dfe.Academies.Api.Infrastructure/MopContext.cs @@ -40,10 +40,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) private static void ConfigureMemberContactDetails(EntityTypeBuilder memberContactDetailsConfiguration) { - memberContactDetailsConfiguration.HasKey(e => e.MemberId); + memberContactDetailsConfiguration.HasKey(e => e.Id); memberContactDetailsConfiguration.ToTable("MemberContactDetails", DEFAULT_SCHEMA); - memberContactDetailsConfiguration.Property(e => e.MemberId).HasColumnName("memberID") + memberContactDetailsConfiguration.Property(e => e.Id).HasColumnName("memberID") .HasConversion( v => v.Value, v => new MemberId(v)); @@ -55,7 +55,7 @@ private static void ConfigureMemberContactDetails(EntityTypeBuilder constituencyConfiguration) { constituencyConfiguration.ToTable("Constituencies", DEFAULT_SCHEMA); - constituencyConfiguration.Property(e => e.ConstituencyId).HasColumnName("constituencyId") + constituencyConfiguration.Property(e => e.Id).HasColumnName("constituencyId") .HasConversion( v => v.Value, v => new ConstituencyId(v)); @@ -78,7 +78,7 @@ private void ConfigureConstituency(EntityTypeBuilder constituencyC .HasOne(c => c.MemberContactDetails) .WithOne() .HasForeignKey(c => c.MemberId) - .HasPrincipalKey(m => m.MemberId); + .HasPrincipalKey(m => m.Id); } diff --git a/Dfe.Academies.Api.Infrastructure/MopRepository.cs b/Dfe.Academies.Api.Infrastructure/MopRepository.cs index effb178e5..47a97574b 100644 --- a/Dfe.Academies.Api.Infrastructure/MopRepository.cs +++ b/Dfe.Academies.Api.Infrastructure/MopRepository.cs @@ -1,7 +1,16 @@ -using Dfe.Academies.Domain.Interfaces.Repositories; +using System.Diagnostics.CodeAnalysis; +using Dfe.Academies.Domain.Common; +using Dfe.Academies.Domain.Interfaces.Repositories; using Dfe.Academies.Infrastructure.Repositories; namespace Dfe.Academies.Infrastructure { - public class MopRepository(MopContext dbContext) : Repository(dbContext), IMopRepository where TEntity : class, new(); + [ExcludeFromCodeCoverage] + public class MopRepository(MopContext dbContext) + : Repository(dbContext), IMopRepository + where TAggregate : class, IAggregateRoot + where TId : ValueObject + { + + } } diff --git a/Dfe.Academies.Api.Infrastructure/MstrRepository.cs b/Dfe.Academies.Api.Infrastructure/MstrRepository.cs index 2130375b0..9f84cd13e 100644 --- a/Dfe.Academies.Api.Infrastructure/MstrRepository.cs +++ b/Dfe.Academies.Api.Infrastructure/MstrRepository.cs @@ -1,7 +1,15 @@ using Dfe.Academies.Infrastructure.Repositories; using Dfe.Academies.Domain.Interfaces.Repositories; +using Dfe.Academies.Domain.Common; +using System.Diagnostics.CodeAnalysis; namespace Dfe.Academies.Infrastructure { - public class MstrRepository(MstrContext dbContext) : Repository(dbContext), IMstrRepository where TEntity : class, new(); + [ExcludeFromCodeCoverage] + public class MstrRepository(MstrContext dbContext) + : Repository(dbContext), IMstrRepository + where TAggregate : class, IAggregateRoot + where TId : ValueObject + { + } } diff --git a/Dfe.Academies.Api.Infrastructure/Repositories/Repository.cs b/Dfe.Academies.Api.Infrastructure/Repositories/Repository.cs index 9b6c3d7cc..8dd48f956 100644 --- a/Dfe.Academies.Api.Infrastructure/Repositories/Repository.cs +++ b/Dfe.Academies.Api.Infrastructure/Repositories/Repository.cs @@ -1,14 +1,17 @@ -using Dfe.Academies.Domain.Interfaces.Repositories; +using Dfe.Academies.Domain.Common; +using Dfe.Academies.Domain.Interfaces.Repositories; using Microsoft.EntityFrameworkCore; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; namespace Dfe.Academies.Infrastructure.Repositories { -#pragma warning disable CS8603 // Possible null reference return, behaviour expected +#pragma warning disable CS8603, S2436 + [ExcludeFromCodeCoverage] - public abstract class Repository : IRepository - where TEntity : class, new() + public abstract class Repository : IRepository + where TAggregate : class, IAggregateRoot + where TId : ValueObject where TDbContext : DbContext { /// @@ -22,170 +25,170 @@ public abstract class Repository : IRepository /// Shorthand for _dbContext.Set /// - protected virtual DbSet DbSet() + protected virtual DbSet DbSet() { - return this.DbContext.Set(); + return this.DbContext.Set(); } /// - public virtual IQueryable Query() => (IQueryable)this.DbSet(); + public virtual IQueryable Query() => (IQueryable)this.DbSet(); /// - public virtual ICollection Fetch(Expression> predicate) + public virtual ICollection Fetch(Expression> predicate) { - return (ICollection)((IQueryable)this.DbSet()).Where(predicate).ToList(); + return (ICollection)((IQueryable)this.DbSet()).Where(predicate).ToList(); } /// - public virtual async Task> FetchAsync( - Expression> predicate, + public virtual async Task> FetchAsync( + Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) { - return (ICollection)await EntityFrameworkQueryableExtensions.ToListAsync(((IQueryable)this.DbSet()).Where(predicate), cancellationToken); + return (ICollection)await EntityFrameworkQueryableExtensions.ToListAsync(((IQueryable)this.DbSet()).Where(predicate), cancellationToken); } /// - public virtual TEntity Find(params object[] keyValues) => this.DbSet().Find(keyValues); + public virtual TAggregate Find(params TId[] keyValues) => this.DbSet().Find(keyValues); /// - public virtual TEntity Find(Expression> predicate) + public virtual TAggregate Find(Expression> predicate) { - return ((IQueryable)this.DbSet()).FirstOrDefault(predicate); + return ((IQueryable)this.DbSet()).FirstOrDefault(predicate); } /// - public virtual async Task FindAsync(params object[] keyValues) + public virtual async Task FindAsync(params TId[] keyValues) { return await this.DbSet().FindAsync(keyValues); } /// - public virtual async Task FindAsync( - Expression> predicate, + public virtual async Task FindAsync( + Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) { - return await EntityFrameworkQueryableExtensions.FirstOrDefaultAsync((IQueryable)this.DbSet(), predicate, cancellationToken); + return await EntityFrameworkQueryableExtensions.FirstOrDefaultAsync((IQueryable)this.DbSet(), predicate, cancellationToken); } /// - public virtual TEntity Get(Expression> predicate) + public virtual TAggregate Get(Expression> predicate) { - return ((IQueryable)this.DbSet()).Single(predicate); + return ((IQueryable)this.DbSet()).Single(predicate); } /// - public virtual TEntity Get(params object[] keyValues) + public virtual TAggregate Get(params TId[] keyValues) { return this.Find(keyValues) ?? throw new InvalidOperationException( - $"Entity type {(object)typeof(TEntity)} is null for primary key {(object)keyValues}"); + $"Entity type {(object)typeof(TAggregate)} is null for primary key {(object)keyValues}"); } /// - public virtual async Task GetAsync(Expression> predicate) + public virtual async Task GetAsync(Expression> predicate) { - return await EntityFrameworkQueryableExtensions.SingleAsync((IQueryable)this.DbSet(), predicate, new CancellationToken()); + return await EntityFrameworkQueryableExtensions.SingleAsync((IQueryable)this.DbSet(), predicate, new CancellationToken()); } /// - public virtual async Task GetAsync(params object[] keyValues) + public virtual async Task GetAsync(params TId[] keyValues) { return await this.FindAsync(keyValues) ?? throw new InvalidOperationException( - $"Entity type {(object)typeof(TEntity)} is null for primary key {(object)keyValues}"); + $"Entity type {(object)typeof(TAggregate)} is null for primary key {(object)keyValues}"); } /// - public virtual TEntity Add(TEntity entity) + public virtual TAggregate Add(TAggregate entity) { - this.DbContext.Add(entity); + this.DbContext.Add(entity); this.DbContext.SaveChanges(); return entity; } /// - public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) + public virtual async Task AddAsync(TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)) { - await this.DbContext.AddAsync(entity, cancellationToken); + await this.DbContext.AddAsync(entity, cancellationToken); await this.DbContext.SaveChangesAsync(cancellationToken); return entity; } /// - public virtual IEnumerable AddRange(ICollection entities) + public virtual IEnumerable AddRange(ICollection entities) { this.DbContext.AddRange((IEnumerable)entities); this.DbContext.SaveChanges(); - return (IEnumerable)entities; + return (IEnumerable)entities; } /// - public virtual async Task> AddRangeAsync( - ICollection entities, + public virtual async Task> AddRangeAsync( + ICollection entities, CancellationToken cancellationToken = default(CancellationToken)) { await this.DbContext.AddRangeAsync((IEnumerable)entities, cancellationToken); await this.DbContext.SaveChangesAsync(cancellationToken); - return (IEnumerable)entities; + return (IEnumerable)entities; } /// - public virtual TEntity Remove(TEntity entity) + public virtual TAggregate Remove(TAggregate entity) { - this.DbContext.Remove(entity); + this.DbContext.Remove(entity); this.DbContext.SaveChanges(); return entity; } /// - public virtual async Task RemoveAsync( - TEntity entity, + public virtual async Task RemoveAsync( + TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)) { - this.DbContext.Remove(entity); + this.DbContext.Remove(entity); await this.DbContext.SaveChangesAsync(cancellationToken); return entity; } /// - public virtual int Delete(Expression> predicate) + public virtual int Delete(Expression> predicate) { return DbSet().Where(predicate).ExecuteDelete(); } /// - public virtual IEnumerable RemoveRange(ICollection entities) + public virtual IEnumerable RemoveRange(ICollection entities) { - this.DbSet().RemoveRange((IEnumerable)entities); + this.DbSet().RemoveRange((IEnumerable)entities); this.DbContext.SaveChanges(); - return (IEnumerable)entities; + return (IEnumerable)entities; } /// - public virtual async Task> RemoveRangeAsync( - ICollection entities, + public virtual async Task> RemoveRangeAsync( + ICollection entities, CancellationToken cancellationToken = default(CancellationToken)) { - this.DbSet().RemoveRange((IEnumerable)entities); + this.DbSet().RemoveRange((IEnumerable)entities); await this.DbContext.SaveChangesAsync(cancellationToken); - return (IEnumerable)entities; + return (IEnumerable)entities; } /// - public virtual TEntity Update(TEntity entity) + public virtual TAggregate Update(TAggregate entity) { - this.DbContext.Update(entity); + this.DbContext.Update(entity); this.DbContext.SaveChanges(); return entity; } /// - public virtual async Task UpdateAsync( - TEntity entity, + public virtual async Task UpdateAsync( + TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)) { - this.DbContext.Update(entity); + this.DbContext.Update(entity); await this.DbContext.SaveChangesAsync(cancellationToken); return entity; } } - #pragma warning restore CS8603 // Possible null reference return +#pragma warning restore CS8603, S2436 } diff --git a/Dfe.Academies.Domain/Common/IAggregateRoot.cs b/Dfe.Academies.Domain/Common/IAggregateRoot.cs new file mode 100644 index 000000000..c132bfe5f --- /dev/null +++ b/Dfe.Academies.Domain/Common/IAggregateRoot.cs @@ -0,0 +1,6 @@ +namespace Dfe.Academies.Domain.Common +{ + public interface IAggregateRoot : IEntity where TId : ValueObject + { + } +} diff --git a/Dfe.Academies.Domain/Common/IEntity.cs b/Dfe.Academies.Domain/Common/IEntity.cs new file mode 100644 index 000000000..451911fb6 --- /dev/null +++ b/Dfe.Academies.Domain/Common/IEntity.cs @@ -0,0 +1,7 @@ +namespace Dfe.Academies.Domain.Common +{ + public interface IEntity where TId : ValueObject + { + TId Id { get; } + } +} diff --git a/Dfe.Academies.Domain/Constituencies/Constituency.cs b/Dfe.Academies.Domain/Constituencies/Constituency.cs index 53d1d69d1..79347628f 100644 --- a/Dfe.Academies.Domain/Constituencies/Constituency.cs +++ b/Dfe.Academies.Domain/Constituencies/Constituency.cs @@ -1,11 +1,12 @@ -using Dfe.Academies.Domain.ValueObjects; +using Dfe.Academies.Domain.Common; +using Dfe.Academies.Domain.ValueObjects; namespace Dfe.Academies.Domain.Constituencies { #pragma warning disable CS8618 - public class Constituency + public class Constituency : IAggregateRoot { - public ConstituencyId ConstituencyId { get; private set; } + public ConstituencyId Id { get; } public MemberId MemberId { get; private set; } public string ConstituencyName { get; private set; } public NameDetails NameDetails { get; private set; } @@ -24,7 +25,7 @@ public Constituency( DateOnly? endDate, MemberContactDetails memberContactDetails) { - ConstituencyId = constituencyId ?? throw new ArgumentNullException(nameof(constituencyId)); + Id = constituencyId ?? throw new ArgumentNullException(nameof(constituencyId)); MemberId = memberId ?? throw new ArgumentNullException(nameof(memberId)); ConstituencyName = constituencyName; NameDetails = nameDetails ?? throw new ArgumentNullException(nameof(nameDetails)); diff --git a/Dfe.Academies.Domain/Constituencies/MemberContactDetails.cs b/Dfe.Academies.Domain/Constituencies/MemberContactDetails.cs index 93ae17b84..806a5d8db 100644 --- a/Dfe.Academies.Domain/Constituencies/MemberContactDetails.cs +++ b/Dfe.Academies.Domain/Constituencies/MemberContactDetails.cs @@ -1,12 +1,13 @@ -using Dfe.Academies.Domain.ValueObjects; +using Dfe.Academies.Domain.Common; +using Dfe.Academies.Domain.ValueObjects; namespace Dfe.Academies.Domain.Constituencies { #pragma warning disable CS8618 - public class MemberContactDetails + public class MemberContactDetails : IEntity { - public MemberId MemberId { get; private set; } + public MemberId Id { get; private set; } public string? Email { get; private set; } public string? Phone { get; private set; } public int TypeId { get; private set; } @@ -21,7 +22,7 @@ public MemberContactDetails( { if (typeId <= 0) throw new ArgumentException("TypeId must be positive", nameof(typeId)); - MemberId = memberId ?? throw new ArgumentNullException(nameof(memberId)); + Id = memberId ?? throw new ArgumentNullException(nameof(memberId)); TypeId = typeId; Email = email; Phone = phone; diff --git a/Dfe.Academies.Domain/Interfaces/Repositories/IMopRepository.cs b/Dfe.Academies.Domain/Interfaces/Repositories/IMopRepository.cs index 606902a14..cd019be63 100644 --- a/Dfe.Academies.Domain/Interfaces/Repositories/IMopRepository.cs +++ b/Dfe.Academies.Domain/Interfaces/Repositories/IMopRepository.cs @@ -1,6 +1,10 @@ -namespace Dfe.Academies.Domain.Interfaces.Repositories +using Dfe.Academies.Domain.Common; + +namespace Dfe.Academies.Domain.Interfaces.Repositories { - public interface IMopRepository : IRepository where TEntity : class, new() + public interface IMopRepository : IRepository + where TAggregate : class, IAggregateRoot + where TId : ValueObject { } } diff --git a/Dfe.Academies.Domain/Interfaces/Repositories/IMstrRepository.cs b/Dfe.Academies.Domain/Interfaces/Repositories/IMstrRepository.cs index 7c12a4397..dc8058a61 100644 --- a/Dfe.Academies.Domain/Interfaces/Repositories/IMstrRepository.cs +++ b/Dfe.Academies.Domain/Interfaces/Repositories/IMstrRepository.cs @@ -1,6 +1,10 @@ -namespace Dfe.Academies.Domain.Interfaces.Repositories +using Dfe.Academies.Domain.Common; + +namespace Dfe.Academies.Domain.Interfaces.Repositories { - public interface IMstrRepository : IRepository where TEntity : class, new() + public interface IMstrRepository : IRepository + where TAggregate : IAggregateRoot + where TId : ValueObject { } } diff --git a/Dfe.Academies.Domain/Interfaces/Repositories/IRepository.cs b/Dfe.Academies.Domain/Interfaces/Repositories/IRepository.cs index cfc7a2465..ca50d764d 100644 --- a/Dfe.Academies.Domain/Interfaces/Repositories/IRepository.cs +++ b/Dfe.Academies.Domain/Interfaces/Repositories/IRepository.cs @@ -1,14 +1,18 @@ -using System.Linq.Expressions; +using Dfe.Academies.Domain.Common; +using System.Linq.Expressions; namespace Dfe.Academies.Domain.Interfaces.Repositories { /// Repository - /// - public interface IRepository where TEntity : class, new() + /// + /// + public interface IRepository + where TAggregate : IAggregateRoot + where TId : ValueObject { /// Returns a queryable (un-resolved!!!!) list of objects. /// Do not expose IQueryable outside of the domain layer - IQueryable Query(); + IQueryable Query(); /// /// Returns an enumerated (resolved!) list of objects based on known query predicate. @@ -16,7 +20,7 @@ namespace Dfe.Academies.Domain.Interfaces.Repositories /// /// /// We know that its the same as doing IQueryable`T.Where(p=> p.value=x).Enumerate but this limits the repo to only ever returning resolved lists. - ICollection Fetch(Expression> predicate); + ICollection Fetch(Expression> predicate); /// /// Asynchronously returns an enumerated (resolved!) list of objects based on known query predicate. @@ -25,8 +29,8 @@ namespace Dfe.Academies.Domain.Interfaces.Repositories /// /// /// We know that its the same as doing IQueryable`T.Where(p=> p.value=x).Enumerate but this limits the repo to only ever returning resolved lists. - Task> FetchAsync( - Expression> predicate, + Task> FetchAsync( + Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)); /// @@ -39,7 +43,7 @@ Task> FetchAsync( /// /// The key values. /// The entity found, or null. - TEntity Find(params object[] keyValues); + TAggregate Find(params TId[] keyValues); /// /// Returns the first entity of a sequence that satisfies a specified condition @@ -47,7 +51,7 @@ Task> FetchAsync( /// /// A function to test an entity for a condition /// The entity found, or null - TEntity Find(Expression> predicate); + TAggregate Find(Expression> predicate); /// /// Asynchronously finds an entity with the given primary key value. If an entity with the @@ -59,7 +63,7 @@ Task> FetchAsync( /// /// The key values. /// The entity found, or null. - Task FindAsync(params object[] keyValues); + Task FindAsync(params TId[] keyValues); /// /// Asynchronously returns the first entity of a sequence that satisfies a specified condition @@ -68,8 +72,8 @@ Task> FetchAsync( /// A function to test an entity for a condition /// /// The entity found, or null - Task FindAsync( - Expression> predicate, + Task FindAsync( + Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)); /// @@ -84,7 +88,7 @@ Task FindAsync( /// The key values. /// The entity found /// If no entity is found in the context or the store -or- more than one entity is found, then an - TEntity Get(params object[] keyValues); + TAggregate Get(params TId[] keyValues); /// /// Gets an entity that satisfies a specified condition, @@ -94,7 +98,7 @@ Task FindAsync( /// /// No entity satisfies the condition in predicate. -or- More than one entity satisfies the condition in predicate. -or- The source sequence is empty. /// - TEntity Get(Expression> predicate); + TAggregate Get(Expression> predicate); /// /// Asynchronously gets an entity with the given primary key value. If an entity with the @@ -108,7 +112,7 @@ Task FindAsync( /// The key values. /// The entity found /// If no entity is found in the context or the store -or- more than one entity is found, then an - Task GetAsync(params object[] keyValues); + Task GetAsync(params TId[] keyValues); /// /// Asynchronously gets an entity that satisfies a specified condition, @@ -118,7 +122,7 @@ Task FindAsync( /// /// No entity satisfies the condition in predicate. -or- More than one entity satisfies the condition in predicate. -or- The source sequence is empty. /// - Task GetAsync(Expression> predicate); + Task GetAsync(Expression> predicate); /// /// Adds the given entity to the context underlying the set in the Added state @@ -129,7 +133,7 @@ Task FindAsync( /// Note that entities that are already in the context in some other state will /// have their state set to Added. Add is a no-op if the entity is already in /// the context in the Added state. - TEntity Add(TEntity entity); + TAggregate Add(TAggregate entity); /// /// Asynchronously adds the given entity to the context underlying the set in the Added state @@ -141,14 +145,14 @@ Task FindAsync( /// Note that entities that are already in the context in some other state will /// have their state set to Added. Add is a no-op if the entity is already in /// the context in the Added state. - Task AddAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)); + Task AddAsync(TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)); /// /// /// /// /// - IEnumerable AddRange(ICollection entities); + IEnumerable AddRange(ICollection entities); /// /// @@ -156,8 +160,8 @@ Task FindAsync( /// /// /// - Task> AddRangeAsync( - ICollection entities, + Task> AddRangeAsync( + ICollection entities, CancellationToken cancellationToken = default(CancellationToken)); /// @@ -171,7 +175,7 @@ Task> AddRangeAsync( /// method will cause it to be detached from the context. This is because an /// Added entity is assumed not to exist in the database such that trying to /// delete it does not make sense. - TEntity Remove(TEntity entity); + TAggregate Remove(TAggregate entity); /// /// Asynchronously marks the given entity as Deleted such that it will be deleted from the database @@ -185,7 +189,7 @@ Task> AddRangeAsync( /// method will cause it to be detached from the context. This is because an /// Added entity is assumed not to exist in the database such that trying to /// delete it does not make sense. - Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)); + Task RemoveAsync(TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)); /// /// Executes a delete statement filtering the rows to be deleted. @@ -198,34 +202,34 @@ Task> AddRangeAsync( /// will not be reflected on any entities that have already been materialized /// in the current contex /// - int Delete(Expression> predicate); + int Delete(Expression> predicate); /// /// Removes the given collection of entities from the DbContext /// /// The collection of entities to remove. - IEnumerable RemoveRange(ICollection entities); + IEnumerable RemoveRange(ICollection entities); /// /// Asynchronously removes the given collection of entities from the DbContext /// /// The collection of entities to remove. /// - Task> RemoveRangeAsync( - ICollection entities, + Task> RemoveRangeAsync( + ICollection entities, CancellationToken cancellationToken = default(CancellationToken)); /// /// Updates the given entity in the DbContext and executes SaveChanges() /// /// The entity to update. - TEntity Update(TEntity entity); + TAggregate Update(TAggregate entity); /// /// Asynchronously updates the given entity in the DbContext and executes SaveChanges() /// /// The entity to update. /// - Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)); + Task UpdateAsync(TAggregate entity, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/PersonsApi/PersonsApi.csproj b/PersonsApi/PersonsApi.csproj index 6b0f1f394..7b560ce98 100644 --- a/PersonsApi/PersonsApi.csproj +++ b/PersonsApi/PersonsApi.csproj @@ -46,6 +46,14 @@ + + + PreserveNewest + true + PreserveNewest + + + diff --git a/PersonsApi/appsettings.Development.json b/PersonsApi/appsettings.Development.json index 0737d3b44..d856d0f9c 100644 --- a/PersonsApi/appsettings.Development.json +++ b/PersonsApi/appsettings.Development.json @@ -21,5 +21,12 @@ ], "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=sip;User ID=sa;Password=StrongPassword905;TrustServerCertificate=True" + }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "platform.education.gov.uk", + "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", + "ClientId": "930a077f-43d0-48cb-9316-1e0430eeaf6b", + "Audience": "api://930a077f-43d0-48cb-9316-1e0430eeaf6b" } } \ No newline at end of file diff --git a/PersonsApi/appsettings.Production.json b/PersonsApi/appsettings.Production.json new file mode 100644 index 000000000..bb40eab6c --- /dev/null +++ b/PersonsApi/appsettings.Production.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "IncludeScopes": true + } + } + }, + "AllowedHosts": "*", + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "platform.education.gov.uk", + "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", + "ClientId": "abae81d8-c7e3-4f28-8349-c43f554a712b", + "Audience": "api://abae81d8-c7e3-4f28-8349-c43f554a712b" + } +} \ No newline at end of file diff --git a/PersonsApi/appsettings.Test.json b/PersonsApi/appsettings.Test.json new file mode 100644 index 000000000..e2c29bd03 --- /dev/null +++ b/PersonsApi/appsettings.Test.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "IncludeScopes": true + } + } + }, + "AllowedHosts": "*", + "ApiKeys": [ + { + "UserName": "Demo User", + "ApiKey": "app-key" + } + ], + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "platform.education.gov.uk", + "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", + "ClientId": "930a077f-43d0-48cb-9316-1e0430eeaf6b", + "Audience": "api://930a077f-43d0-48cb-9316-1e0430eeaf6b" + } +} \ No newline at end of file diff --git a/PersonsApi/appsettings.json b/PersonsApi/appsettings.json index 4deea15f6..7f6a32871 100644 --- a/PersonsApi/appsettings.json +++ b/PersonsApi/appsettings.json @@ -23,16 +23,6 @@ "ApiKey": "app-key" } ], - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "Domain": "platform.education.gov.uk", - "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", - "ClientId": "930a077f-43d0-48cb-9316-1e0430eeaf6b", - "Audience": "api://930a077f-43d0-48cb-9316-1e0430eeaf6b" - }, - "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Database=sip;User Id=sa;TrustServerCertificate=True;Password=StrongPassword905" - }, "SyncAcademyConversionProjectsSchedule": "0 0/15 * * * *", "Serilog": { "Using": [ @@ -55,7 +45,8 @@ "CacheSettings": { "DefaultDurationInSeconds": 60, "Durations": { - "GetMemberOfParliamentByConstituencyAsync": 30 + "GetMembersOfParliamentByConstituenciesQueryHandler": 86400, + "GetMemberOfParliamentByConstituencyQueryHandler": 86400 } }, "Authorization": { diff --git a/Tests/Dfe.Academies.Domain.Tests/Aggregates/ConstituencyTests.cs b/Tests/Dfe.Academies.Domain.Tests/Aggregates/ConstituencyTests.cs new file mode 100644 index 000000000..763dd9ca0 --- /dev/null +++ b/Tests/Dfe.Academies.Domain.Tests/Aggregates/ConstituencyTests.cs @@ -0,0 +1,62 @@ +using Dfe.Academies.Domain.Constituencies; +using Dfe.Academies.Domain.ValueObjects; +using Dfe.Academies.Testing.Common.Attributes; +using Dfe.Academies.Testing.Common.Customizations; +using Dfe.Academies.Testing.Common.Customizations.Models; + +namespace Dfe.Academies.Domain.Tests.Aggregates +{ + public class ConstituencyTests + { + [Theory] + [CustomAutoData(typeof(MemberOfParliamentCustomization), typeof(DateOnlyCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenConstituencyIdIsNull( + MemberId memberId, + string constituencyName, + NameDetails nameDetails, + DateTime lastRefresh, + DateOnly? endDate, + MemberContactDetails memberContactDetails) + { + // Act & Assert + var exception = Assert.Throws(() => + new Constituency(null!, memberId, constituencyName, nameDetails, lastRefresh, endDate, memberContactDetails)); + + Assert.Equal("constituencyId", exception.ParamName); + } + + [Theory] + [CustomAutoData(typeof(MemberOfParliamentCustomization), typeof(DateOnlyCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenMemberIdIsNull( + ConstituencyId constituencyId, + string constituencyName, + NameDetails nameDetails, + DateTime lastRefresh, + DateOnly? endDate, + MemberContactDetails memberContactDetails) + { + // Act & Assert + var exception = Assert.Throws(() => + new Constituency(constituencyId, null!, constituencyName, nameDetails, lastRefresh, endDate, memberContactDetails)); + + Assert.Equal("memberId", exception.ParamName); + } + + [Theory] + [CustomAutoData(typeof(MemberOfParliamentCustomization), typeof(DateOnlyCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenNameDetailsIsNull( + ConstituencyId constituencyId, + MemberId memberId, + string constituencyName, + DateTime lastRefresh, + DateOnly? endDate, + MemberContactDetails memberContactDetails) + { + // Act & Assert + var exception = Assert.Throws(() => + new Constituency(constituencyId, memberId, constituencyName, null!, lastRefresh, endDate, memberContactDetails)); + + Assert.Equal("nameDetails", exception.ParamName); + } + } +} diff --git a/Tests/Dfe.Academies.Domain.Tests/Aggregates/MemberContactDetailsTests.cs b/Tests/Dfe.Academies.Domain.Tests/Aggregates/MemberContactDetailsTests.cs new file mode 100644 index 000000000..5cfc821f1 --- /dev/null +++ b/Tests/Dfe.Academies.Domain.Tests/Aggregates/MemberContactDetailsTests.cs @@ -0,0 +1,53 @@ +using Dfe.Academies.Domain.Constituencies; +using Dfe.Academies.Domain.ValueObjects; +using Dfe.Academies.Testing.Common.Attributes; + +namespace Dfe.Academies.Domain.Tests.Aggregates +{ + public class MemberContactDetailsTests + { + [Theory] + [CustomAutoData] + public void Constructor_ShouldThrowArgumentNullException_WhenMemberIdIsNull( + int typeId, + string? email, + string? phone) + { + // Act & Assert + var exception = Assert.Throws(() => + new MemberContactDetails(null!, typeId, email, phone)); + + Assert.Equal("memberId", exception.ParamName); + } + + [Theory] + [CustomAutoData] + public void Constructor_ShouldThrowArgumentException_WhenTypeIdIsNotPositive( + MemberId memberId, + string? email, + string? phone) + { + // Act & Assert + var exception = Assert.Throws(() => + new MemberContactDetails(memberId, -1, email, phone)); + + Assert.Contains("TypeId must be positive", exception.Message); + Assert.Equal("typeId", exception.ParamName); + } + + [Theory] + [CustomAutoData] + public void Constructor_ShouldThrowArgumentException_WhenTypeIdIsZero( + MemberId memberId, + string? email, + string? phone) + { + // Act & Assert + var exception = Assert.Throws(() => + new MemberContactDetails(memberId, 0, email, phone)); + + Assert.Contains("TypeId must be positive", exception.Message); + Assert.Equal("typeId", exception.ParamName); + } + } +} diff --git a/Tests/Dfe.Academies.Domain.Tests/Dfe.Academies.Domain.Tests.csproj b/Tests/Dfe.Academies.Domain.Tests/Dfe.Academies.Domain.Tests.csproj new file mode 100644 index 000000000..46c80eed0 --- /dev/null +++ b/Tests/Dfe.Academies.Domain.Tests/Dfe.Academies.Domain.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Tests/Dfe.Academies.Testing.Common/Customizations/DateOnlyCustomization.cs b/Tests/Dfe.Academies.Testing.Common/Customizations/DateOnlyCustomization.cs new file mode 100644 index 000000000..7729d5f9b --- /dev/null +++ b/Tests/Dfe.Academies.Testing.Common/Customizations/DateOnlyCustomization.cs @@ -0,0 +1,14 @@ +using AutoFixture; + +namespace Dfe.Academies.Testing.Common.Customizations +{ + public class DateOnlyCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customize(composer => + composer.FromFactory(() => + DateOnly.FromDateTime(fixture.Create()))); + } + } +} diff --git a/Tests/Dfe.Academies.Testing.Common/Dfe.Academies.Testing.Common.csproj b/Tests/Dfe.Academies.Testing.Common/Dfe.Academies.Testing.Common.csproj index b79498a03..e434be00e 100644 --- a/Tests/Dfe.Academies.Testing.Common/Dfe.Academies.Testing.Common.csproj +++ b/Tests/Dfe.Academies.Testing.Common/Dfe.Academies.Testing.Common.csproj @@ -7,8 +7,12 @@ - - + + + + + + diff --git a/TramsDataApi.sln b/TramsDataApi.sln index 3359e0c82..d894b89e9 100644 --- a/TramsDataApi.sln +++ b/TramsDataApi.sln @@ -28,7 +28,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.Application.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.PersonsApi.Tests.Integration", "Tests\Dfe.Academies.PersonsApi.Tests.Integration\Dfe.Academies.PersonsApi.Tests.Integration.csproj", "{EB0D20C1-4818-44DF-97A7-276C9F96CC64}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Academies.Testing.Common", "Tests\Dfe.Academies.Testing.Common\Dfe.Academies.Testing.Common.csproj", "{777C300F-FBB1-402A-A850-1D26417FA412}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.Testing.Common", "Tests\Dfe.Academies.Testing.Common\Dfe.Academies.Testing.Common.csproj", "{777C300F-FBB1-402A-A850-1D26417FA412}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Academies.Domain.Tests", "Tests\Dfe.Academies.Domain.Tests\Dfe.Academies.Domain.Tests.csproj", "{82966208-86EA-4459-8D99-FED765D07D82}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -76,6 +78,10 @@ Global {777C300F-FBB1-402A-A850-1D26417FA412}.Debug|Any CPU.Build.0 = Debug|Any CPU {777C300F-FBB1-402A-A850-1D26417FA412}.Release|Any CPU.ActiveCfg = Release|Any CPU {777C300F-FBB1-402A-A850-1D26417FA412}.Release|Any CPU.Build.0 = Release|Any CPU + {82966208-86EA-4459-8D99-FED765D07D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82966208-86EA-4459-8D99-FED765D07D82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82966208-86EA-4459-8D99-FED765D07D82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82966208-86EA-4459-8D99-FED765D07D82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -84,6 +90,7 @@ Global {9FDF7C25-083F-4404-BE64-82EF26EED072} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} {EB0D20C1-4818-44DF-97A7-276C9F96CC64} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} {777C300F-FBB1-402A-A850-1D26417FA412} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} + {82966208-86EA-4459-8D99-FED765D07D82} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F0704299-A9C2-448A-B816-E5BCCB345AF8}