Skip to content

Commit

Permalink
Merge pull request #65 from gpproton/dev
Browse files Browse the repository at this point in the history
Added specification improvements
  • Loading branch information
gpproton authored May 26, 2023
2 parents 948e3b3 + 9f59d03 commit f26a6f0
Show file tree
Hide file tree
Showing 24 changed files with 284 additions and 88 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<DotnetVersion>8.0.0-preview.4.23259.5</DotnetVersion>
<EfcoreVersion>8.0.0-preview.4.23259.3</EfcoreVersion>
<NextendedVersion>7.0.22</NextendedVersion>
<Net7Version>7.0.5</Net7Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
12 changes: 10 additions & 2 deletions samples/aspnet-sample/Data/MigrationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// limitations under the License.

using Microsoft.EntityFrameworkCore;
using Proton.Common.AspNetSample.Features.CategoryModule;

namespace Proton.Common.AspNetSample.Data;

Expand All @@ -31,8 +32,15 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
if (context.Database.IsRelational())
await context.Database.MigrateAsync(cancellationToken);

// TODO: Setup sample data seeding.
// await context.SaveChangesAsync(cancellationToken);
var anyCategory = await context.Categories.AnyAsync(cancellationToken);
if (!anyCategory) {
await context.Categories.AddRangeAsync(new List<Category> {
new() { Name = "test-1" },
new() { Name = "test-2" },
new() { Name = "test-3" }
}, cancellationToken);
}
await context.SaveChangesAsync(cancellationToken);

_logger.LogInformation("Completed migration & Seed process...");
}
Expand Down
17 changes: 16 additions & 1 deletion samples/aspnet-sample/Data/ServiceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,29 @@
// limitations under the License.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Proton.Common.AspNetSample.Features.CategoryModule;
using Proton.Common.AspNetSample.Features.PostModule;

namespace Proton.Common.AspNetSample.Data;

public class ServiceContext : DbContext {
public ServiceContext(DbContextOptions<ServiceContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder) { }

protected override void OnModelCreating(ModelBuilder modelBuilder) {
if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") {
foreach (var entityType in modelBuilder.Model.GetEntityTypes()) {
var properties = entityType.ClrType.GetProperties()
.Where(p => p.PropertyType == typeof(DateTimeOffset)
|| p.PropertyType == typeof(DateTimeOffset?));
foreach (var property in properties)
modelBuilder
.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(new DateTimeOffsetToBinaryConverter());
}
}
}

public virtual DbSet<Category> Categories { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

using Proton.Common.AspNet.Feature;
using Proton.Common.Enums;
using Proton.Common.Filters;

namespace Proton.Common.AspNetSample.Features.CategoryModule;

Expand All @@ -25,7 +26,7 @@ public override IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoin
EndpointType.Delete,
EndpointType.DeleteRange
};
var group = SetupGroup<Category, Guid>(endpoints, types);
var group = SetupGroup<Category, Guid>(endpoints, types, specType: typeof(CategorySpec));

return group;
}
Expand Down
25 changes: 25 additions & 0 deletions samples/aspnet-sample/Features/CategoryModule/CategorySpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2022 - 2023 Godwin peter .O (me@godwin.dev)
//
// Licensed under the MIT License;
// you may not use this file except in compliance with the License.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Ardalis.Specification;
using Microsoft.EntityFrameworkCore;
using Proton.Common.Interfaces;

namespace Proton.Common.AspNetSample.Features.CategoryModule;

public sealed class CategorySpec : Specification<Category> {
public CategorySpec(IPageFilter filter) {
var search = filter.Search ?? string.Empty;
var text = search.ToLower().Split(" ").ToList().Select(x => x);

Query.Where(x => x.Name != String.Empty && x.Name.Length > 3 && text.Any(p => EF.Functions.Like(x.Name.ToLower(), $"%" + p + "%")))
.OrderByDescending(b => b.CreatedAt);
}
}
43 changes: 28 additions & 15 deletions src/aspnet/Feature/GenericFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Ardalis.Specification;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Nextended.Core.Extensions;
using Proton.Common.AspNet.Filters;
using Proton.Common.AspNet.Service;
using Proton.Common.EFCore.Interfaces;
using Proton.Common.Enums;
using Proton.Common.Filters;
using Proton.Common.Response;

namespace Proton.Common.AspNet.Feature;

public class TempParam<TId> where TId : notnull {
internal sealed class EndpointParam<TId> where TId : notnull {
public TId Id { get; set; } = default!;
}

Expand All @@ -30,8 +30,21 @@ public abstract class GenericFeature : IFeature {

public abstract IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints);

protected virtual IEndpointRouteBuilder SetupGroup<TEntity, TId>(IEndpointRouteBuilder endpoints, List<EndpointType>? types = null, string root = "/api/v1")
where TEntity : class, IAggregateRoot
protected virtual IEndpointRouteBuilder SetupGroup<TEntity, TId>(
IEndpointRouteBuilder endpoints,
List<EndpointType>? types = null,
string root = "/api/v1",
Type? specType = null)
where TEntity : class, IAggregateRoot, IResponse
where TId : notnull => SetupGroup<TEntity, TEntity, TId>(endpoints, types, root, specType);

protected virtual IEndpointRouteBuilder SetupGroup<TEntity, TResponse, TId>(
IEndpointRouteBuilder endpoints,
List<EndpointType>? types = null,
string root = "/api/v1",
Type? specType = null)
where TEntity : class, IAggregateRoot, IResponse
where TResponse : class, IResponse
where TId : notnull {
var type = typeof(TEntity);
var name = type.Name.ToLower();
Expand All @@ -42,13 +55,13 @@ protected virtual IEndpointRouteBuilder SetupGroup<TEntity, TId>(IEndpointRouteB
var active = types ?? new List<EndpointType> { EndpointType.GetAll, EndpointType.GetById };

if (active.Contains(EndpointType.GetAll)) {
group.MapGet(String.Empty, async (IGenericService<TEntity> sv, [AsParameters] PageFilter filter) =>
await sv.GetAllAsync(filter))
group.MapGet(String.Empty, async (IGenericService<TEntity, TResponse> sv, [AsParameters] PagedFilter filter) =>
await sv.GetAllAsync(filter, specType))
.WithName($"GetAll{name}");
}

if (active.Contains(EndpointType.GetById)) {
group.MapGet("/{id}", async (IGenericService<TEntity> sv, [AsParameters] TempParam<TId> parameters) =>
group.MapGet("/{id}", async (IGenericService<TEntity, TResponse> sv, [AsParameters] EndpointParam<TId> parameters) =>
await sv.GetByIdAsync(parameters.Id)
).WithName($"Get{name}ById");
}
Expand All @@ -57,42 +70,42 @@ await sv.GetByIdAsync(parameters.Id)

// Create item
if (active.Contains(EndpointType.Create)) {
group.MapPost(String.Empty, async (IGenericService<TEntity> sv, TEntity value) =>
group.MapPost(String.Empty, async (IGenericService<TEntity, TResponse> sv, TEntity value) =>
await sv.CreateAsync(value))
.WithName($"Create{name}");
}

// Create multiple items
if (active.Contains(EndpointType.CreateRange)) {
group.MapPost("/multiple", async (IGenericService<TEntity> sv, IEnumerable<TEntity> values) =>
group.MapPost("/multiple", async (IGenericService<TEntity, TResponse> sv, IEnumerable<TEntity> values) =>
await sv.CreateRangeAsync(values))
.WithName($"CreateMultiple{name}");
}

// Update item
if (active.Contains(EndpointType.Update)) {
group.MapPut(String.Empty, async (IGenericService<TEntity> sv, TEntity value) =>
group.MapPut(String.Empty, async (IGenericService<TEntity, TResponse> sv, TEntity value) =>
await sv.UpdateAsync(value))
.WithName($"Update{name}");
}

// Update multiple items
if (active.Contains(EndpointType.UpdateRange)) {
group.MapPut("/multiple", async (IGenericService<TEntity> sv, IEnumerable<TEntity> values) =>
group.MapPut("/multiple", async (IGenericService<TEntity, TResponse> sv, IEnumerable<TEntity> values) =>
await sv.UpdateRangeAsync(values))
.WithName($"UpdateMultiple{name}");
}

// Delete item by id
if (active.Contains(EndpointType.Delete)) {
group.MapDelete("/{id}", async (IGenericService<TEntity> sv, [AsParameters] TempParam<TId> parameters) =>
group.MapDelete("/{id}", async (IGenericService<TEntity, TResponse> sv, [AsParameters] EndpointParam<TId> parameters) =>
await sv.GetByIdAsync(parameters.Id))
.WithName($"Delete{name}");
}

// Delete multiple items
if (active.Contains(EndpointType.DeleteRange)) {
group.MapDelete("/multiple", async (IGenericService<TEntity> sv, IEnumerable<TEntity> values) =>
group.MapDelete("/multiple", async (IGenericService<TEntity, TResponse> sv, IEnumerable<TEntity> values) =>
await sv.DeleteRangeAsync(values))
.WithName($"DeleteMultiple{name}");
}
Expand Down
11 changes: 11 additions & 0 deletions src/aspnet/Globals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2022 - 2023 Godwin peter .O (me@godwin.dev)
//
// Licensed under the MIT License;
// you may not use this file except in compliance with the License.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

global using Nextended.Core.Extensions;
4 changes: 3 additions & 1 deletion src/aspnet/Proton.Common.AspNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@

<ItemGroup Condition="$(TargetFramework.Contains('net8'))">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.EntityFrameworkCore" VersionOverride="$(EfcoreVersion)" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.Contains('net7'))">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" VersionOverride="7.0.5" />
<!-- <PackageReference Include="Microsoft.AspNetCore.OpenApi" VersionOverride="$(Net7Version)" /> -->
<!-- <PackageReference Include="Microsoft.EntityFrameworkCore" VersionOverride="$(Net7Version)" /> -->
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 2 additions & 6 deletions src/aspnet/Service/GenericListSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@
namespace Proton.Common.AspNet.Service;

public sealed class GenericListSpec<TEntity> : Specification<TEntity> {
public GenericListSpec(IPageFilter? filter) {
var check = filter ?? new PageFilter();
var page = check.Page ?? 1;
var size = check.Size ?? 25;
// var search = check.Search?.Split(" ").ToList().Select(x => x.ToLower());
// Query.Take(size).Take(page - 1 * size);
public GenericListSpec() {
Query.Where(x => x != null);
}
}
58 changes: 18 additions & 40 deletions src/aspnet/Service/GenericService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// limitations under the License.


using Ardalis.Specification;
using Microsoft.EntityFrameworkCore;
using Proton.Common.EFCore.Interfaces;
using Proton.Common.EFCore.Repository;
Expand All @@ -17,50 +18,29 @@

namespace Proton.Common.AspNet.Service;

public sealed class GenericService<TEntity>(IRepository<TEntity> repo) :
IGenericService<TEntity> where TEntity : class, IAggregateRoot {
public sealed class GenericService<TEntity>(IRepository<TEntity> repo, IGenericService<TEntity, TEntity> root) :
IGenericService<TEntity> where TEntity : class, IAggregateRoot, IResponse {

public async Task<PagedResponse<TEntity>> GetAllAsync(IPageFilter? filter) {
var count = await repo.GetQueryable().CountAsync();
var result = await repo.GetAll(new GenericListSpec<TEntity>(filter)).ToListAsync();
var page = filter!.Page ?? 1;
var size = filter!.Size ?? 25;
return new PagedResponse<TEntity>(result, page, size, count);
}

public async Task<Response<TEntity?>> GetByIdAsync<TId>(TId id) where TId : notnull {
var result = await repo.GetByIdAsync(id);
return new Response<TEntity?>(result);
}
public async Task<PagedResponse<TEntity>> GetAllAsync(IPageFilter? filter, Type? type) =>
await root.GetAllAsync(filter, type);

public async Task<Response<TEntity>> CreateAsync(TEntity value) {
var result = await repo.AddAsync(value);
return new Response<TEntity>(result);
}

public async Task<PagedResponse<TEntity>> CreateRangeAsync(IEnumerable<TEntity> values) {
var result = await repo.AddRangeAsync(values);
return new PagedResponse<TEntity>(result);
}
public async Task<Response<TEntity?>> GetByIdAsync<TId>(TId id) where TId : notnull =>
await root.GetByIdAsync(id);

public async Task<Response<TEntity?>> UpdateAsync(TEntity value) {
await repo.UpdateAsync(value);
return new Response<TEntity?>(value);
}
public async Task<Response<TEntity>> CreateAsync(TEntity value) =>
await root.CreateAsync(value);

public async Task<PagedResponse<TEntity>> UpdateRangeAsync(IEnumerable<TEntity> values) {
IEnumerable<TEntity> aggregateRoots = values.ToList();
await repo.UpdateRangeAsync(aggregateRoots);
public async Task<PagedResponse<TEntity>> CreateRangeAsync(IEnumerable<TEntity> values) =>
await root.CreateRangeAsync(values);

return new PagedResponse<TEntity>(aggregateRoots);
}
public async Task<Response<TEntity>> UpdateAsync(TEntity value) =>
await root.UpdateAsync(value);

public async Task<Response<TEntity?>> DeleteAsync<TId>(TId id) where TId : notnull {
var item = await repo.GetByIdAsync(id);
if (item is not null) await repo.DeleteAsync(item);
public async Task<PagedResponse<TEntity>> UpdateRangeAsync(IEnumerable<TEntity> values) =>
await root.UpdateRangeAsync(values);

return new Response<TEntity?>(item, "", item != null);
}
public async Task<Response<TEntity?>> DeleteAsync<TId>(TId id) where TId : notnull =>
await root.DeleteAsync(id);

public async Task<PagedResponse<TEntity>> DeleteRangeAsync(IEnumerable<TEntity> values) {
IEnumerable<TEntity> aggregateRoots = values.ToList();
Expand All @@ -69,7 +49,5 @@ public async Task<PagedResponse<TEntity>> DeleteRangeAsync(IEnumerable<TEntity>
return new PagedResponse<TEntity>(aggregateRoots);
}

public async Task ClearAsync() {
await repo.ClearAsync();
}
public async Task ClearAsync() => await root.ClearAsync();
}
Loading

0 comments on commit f26a6f0

Please sign in to comment.