From c7532640e8e91433a3e386d7d9e54851f46a8598 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 18 Jan 2024 12:00:42 +0000 Subject: [PATCH] Feat: Added support for minimal APIs (#43) --- .../ApplicationDbContext.cs | 0 .../Controllers/HomeController.cs | 0 .../Controllers/UsersController.cs | 0 .../{example1 => controller}/Dtos/UserDto.cs | 0 .../{example1 => controller}/Entities/User.cs | 0 .../Interfaces/IUserService.cs | 0 .../Profiles/AutoMapperProfile.cs | 0 examples/{example1 => controller}/Program.cs | 2 +- .../Properties/launchSettings.json | 0 .../SearchBinders/UserDtoSearchBinder.cs | 0 .../Services/UserService.cs | 0 .../appsettings.Development.json | 0 .../{example1 => controller}/appsettings.json | 0 .../controller.csproj} | 0 examples/minimal/ApplicationDbContext.cs | 8 +++ examples/minimal/Dtos/UserDto.cs | 19 +++++ examples/minimal/Entities/User.cs | 26 +++++++ examples/minimal/Interfaces/IUserService.cs | 4 ++ .../minimal/Profiles/AutoMapperProfile.cs | 9 +++ examples/minimal/Program.cs | 69 +++++++++++++++++++ .../minimal/Properties/launchSettings.json | 41 +++++++++++ .../SearchBinders/UserDtoSearchBinder.cs | 11 +++ examples/minimal/Services/UserService.cs | 22 ++++++ examples/minimal/minimal.csproj | 17 +++++ src/Extensions/QueryableExtension.cs | 6 +- src/Query.cs | 14 ++-- tests/Tests.cs | 4 +- 27 files changed, 239 insertions(+), 13 deletions(-) rename examples/{example1 => controller}/ApplicationDbContext.cs (100%) rename examples/{example1 => controller}/Controllers/HomeController.cs (100%) rename examples/{example1 => controller}/Controllers/UsersController.cs (100%) rename examples/{example1 => controller}/Dtos/UserDto.cs (100%) rename examples/{example1 => controller}/Entities/User.cs (100%) rename examples/{example1 => controller}/Interfaces/IUserService.cs (100%) rename examples/{example1 => controller}/Profiles/AutoMapperProfile.cs (100%) rename examples/{example1 => controller}/Program.cs (94%) rename examples/{example1 => controller}/Properties/launchSettings.json (100%) rename examples/{example1 => controller}/SearchBinders/UserDtoSearchBinder.cs (100%) rename examples/{example1 => controller}/Services/UserService.cs (100%) rename examples/{example1 => controller}/appsettings.Development.json (100%) rename examples/{example1 => controller}/appsettings.json (100%) rename examples/{example1/example1.csproj => controller/controller.csproj} (100%) create mode 100644 examples/minimal/ApplicationDbContext.cs create mode 100644 examples/minimal/Dtos/UserDto.cs create mode 100644 examples/minimal/Entities/User.cs create mode 100644 examples/minimal/Interfaces/IUserService.cs create mode 100644 examples/minimal/Profiles/AutoMapperProfile.cs create mode 100644 examples/minimal/Program.cs create mode 100644 examples/minimal/Properties/launchSettings.json create mode 100644 examples/minimal/SearchBinders/UserDtoSearchBinder.cs create mode 100644 examples/minimal/Services/UserService.cs create mode 100644 examples/minimal/minimal.csproj diff --git a/examples/example1/ApplicationDbContext.cs b/examples/controller/ApplicationDbContext.cs similarity index 100% rename from examples/example1/ApplicationDbContext.cs rename to examples/controller/ApplicationDbContext.cs diff --git a/examples/example1/Controllers/HomeController.cs b/examples/controller/Controllers/HomeController.cs similarity index 100% rename from examples/example1/Controllers/HomeController.cs rename to examples/controller/Controllers/HomeController.cs diff --git a/examples/example1/Controllers/UsersController.cs b/examples/controller/Controllers/UsersController.cs similarity index 100% rename from examples/example1/Controllers/UsersController.cs rename to examples/controller/Controllers/UsersController.cs diff --git a/examples/example1/Dtos/UserDto.cs b/examples/controller/Dtos/UserDto.cs similarity index 100% rename from examples/example1/Dtos/UserDto.cs rename to examples/controller/Dtos/UserDto.cs diff --git a/examples/example1/Entities/User.cs b/examples/controller/Entities/User.cs similarity index 100% rename from examples/example1/Entities/User.cs rename to examples/controller/Entities/User.cs diff --git a/examples/example1/Interfaces/IUserService.cs b/examples/controller/Interfaces/IUserService.cs similarity index 100% rename from examples/example1/Interfaces/IUserService.cs rename to examples/controller/Interfaces/IUserService.cs diff --git a/examples/example1/Profiles/AutoMapperProfile.cs b/examples/controller/Profiles/AutoMapperProfile.cs similarity index 100% rename from examples/example1/Profiles/AutoMapperProfile.cs rename to examples/controller/Profiles/AutoMapperProfile.cs diff --git a/examples/example1/Program.cs b/examples/controller/Program.cs similarity index 94% rename from examples/example1/Program.cs rename to examples/controller/Program.cs index 5615cde..8ce4fd0 100644 --- a/examples/example1/Program.cs +++ b/examples/controller/Program.cs @@ -16,7 +16,7 @@ builder.Services.AddDbContext(options => { - options.UseInMemoryDatabase("example1"); + options.UseInMemoryDatabase("controller"); }); builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); diff --git a/examples/example1/Properties/launchSettings.json b/examples/controller/Properties/launchSettings.json similarity index 100% rename from examples/example1/Properties/launchSettings.json rename to examples/controller/Properties/launchSettings.json diff --git a/examples/example1/SearchBinders/UserDtoSearchBinder.cs b/examples/controller/SearchBinders/UserDtoSearchBinder.cs similarity index 100% rename from examples/example1/SearchBinders/UserDtoSearchBinder.cs rename to examples/controller/SearchBinders/UserDtoSearchBinder.cs diff --git a/examples/example1/Services/UserService.cs b/examples/controller/Services/UserService.cs similarity index 100% rename from examples/example1/Services/UserService.cs rename to examples/controller/Services/UserService.cs diff --git a/examples/example1/appsettings.Development.json b/examples/controller/appsettings.Development.json similarity index 100% rename from examples/example1/appsettings.Development.json rename to examples/controller/appsettings.Development.json diff --git a/examples/example1/appsettings.json b/examples/controller/appsettings.json similarity index 100% rename from examples/example1/appsettings.json rename to examples/controller/appsettings.json diff --git a/examples/example1/example1.csproj b/examples/controller/controller.csproj similarity index 100% rename from examples/example1/example1.csproj rename to examples/controller/controller.csproj diff --git a/examples/minimal/ApplicationDbContext.cs b/examples/minimal/ApplicationDbContext.cs new file mode 100644 index 0000000..5735ca9 --- /dev/null +++ b/examples/minimal/ApplicationDbContext.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) { } + + public DbSet Users => Set(); +} \ No newline at end of file diff --git a/examples/minimal/Dtos/UserDto.cs b/examples/minimal/Dtos/UserDto.cs new file mode 100644 index 0000000..fa76287 --- /dev/null +++ b/examples/minimal/Dtos/UserDto.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +public record UserDto +{ + public Guid Id { get; set; } + + public string Firstname { get; set; } = string.Empty; + + public string Lastname { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public string AvatarUrl { get; set; } = string.Empty; + + [JsonPropertyName("displayName")] + public string UserName { get; set; } = string.Empty; + + public string Gender { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/examples/minimal/Entities/User.cs b/examples/minimal/Entities/User.cs new file mode 100644 index 0000000..d7e4f17 --- /dev/null +++ b/examples/minimal/Entities/User.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Bogus.DataSets; + +public record User +{ + public Guid Id { get; set; } + + [Column(TypeName = "varchar(128)")] + public string Firstname { get; set; } = string.Empty; + + [Column(TypeName = "varchar(128)")] + public string Lastname { get; set; } = string.Empty; + + [Column(TypeName = "varchar(320)")] + public string Email { get; set; } = string.Empty; + + [Column(TypeName = "varchar(1024)")] + public string AvatarUrl { get; set; } = string.Empty; + public bool IsDeleted { get; set; } + + [Column(TypeName = "varchar(64)")] + public string UserName { get; set; } = string.Empty; + + [Column("PersonSex", TypeName = "varchar(32)")] + public string Gender { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/examples/minimal/Interfaces/IUserService.cs b/examples/minimal/Interfaces/IUserService.cs new file mode 100644 index 0000000..90ba4e0 --- /dev/null +++ b/examples/minimal/Interfaces/IUserService.cs @@ -0,0 +1,4 @@ +public interface IUserService +{ + IQueryable GetAllUsers(); +} \ No newline at end of file diff --git a/examples/minimal/Profiles/AutoMapperProfile.cs b/examples/minimal/Profiles/AutoMapperProfile.cs new file mode 100644 index 0000000..cd2efb2 --- /dev/null +++ b/examples/minimal/Profiles/AutoMapperProfile.cs @@ -0,0 +1,9 @@ +using AutoMapper; + +public class AutoMapperProfile : Profile +{ + public AutoMapperProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/examples/minimal/Program.cs b/examples/minimal/Program.cs new file mode 100644 index 0000000..c2516f8 --- /dev/null +++ b/examples/minimal/Program.cs @@ -0,0 +1,69 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using Bogus; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); + +builder.Services.AddSwaggerGen(options => +{ + options.OperationFilter(); +}); + +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("minimal"); +}); + +builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); + +builder.Services.AddScoped(); + +builder.Services.AddSingleton, UserDtoSearchBinder>(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); + + using (var scope = app.Services.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(); + + // Seed data + if (!context.Users.Any()) + { + var users = new Faker() + .RuleFor(x => x.Firstname, f => f.Person.FirstName) + .RuleFor(x => x.Lastname, f => f.Person.LastName) + .RuleFor(x => x.Email, f => f.Person.Email) + .RuleFor(x => x.AvatarUrl, f => f.Internet.Avatar()) + .RuleFor(x => x.UserName, f => f.Person.UserName) + .RuleFor(x => x.Gender, f => f.Person.Gender.ToString()) + .RuleFor(x => x.IsDeleted, f => f.Random.Bool()); + + context.Users.AddRange(users.Generate(1_000)); + context.SaveChanges(); + + Console.WriteLine("Seeded 1,000 fake users!"); + } + } +} + +app.UseHttpsRedirection(); + +app.MapGet("/users", (ApplicationDbContext db, [FromServices] IUserService userService, [AsParameters] Query query) => +{ + var (users, count) = userService.GetAllUsers().Apply(query); + + return TypedResults.Ok(new PagedResponse(users.ToDynamicList(), count)); +}); + +app.Run(); diff --git a/examples/minimal/Properties/launchSettings.json b/examples/minimal/Properties/launchSettings.json new file mode 100644 index 0000000..bf662f3 --- /dev/null +++ b/examples/minimal/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42100", + "sslPort": 44374 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5240", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7077;http://localhost:5240", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/minimal/SearchBinders/UserDtoSearchBinder.cs b/examples/minimal/SearchBinders/UserDtoSearchBinder.cs new file mode 100644 index 0000000..6ce15e9 --- /dev/null +++ b/examples/minimal/SearchBinders/UserDtoSearchBinder.cs @@ -0,0 +1,11 @@ +using System.Linq.Expressions; + +public class UserDtoSearchBinder : ISearchBinder +{ + public Expression> Bind(string searchTerm) + { + Expression> exp = x => x.Firstname.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || x.Lastname.Contains(searchTerm, StringComparison.OrdinalIgnoreCase); + + return exp; + } +} \ No newline at end of file diff --git a/examples/minimal/Services/UserService.cs b/examples/minimal/Services/UserService.cs new file mode 100644 index 0000000..edd5275 --- /dev/null +++ b/examples/minimal/Services/UserService.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +public class UserService : IUserService +{ + private readonly ApplicationDbContext _context; + private readonly IMapper _mapper; + + public UserService(ApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public IQueryable GetAllUsers() + { + return _context.Users.AsNoTracking() + .Where(x => !x.IsDeleted) + .ProjectTo(_mapper.ConfigurationProvider); + } +} \ No newline at end of file diff --git a/examples/minimal/minimal.csproj b/examples/minimal/minimal.csproj new file mode 100644 index 0000000..b21da3e --- /dev/null +++ b/examples/minimal/minimal.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Extensions/QueryableExtension.cs b/src/Extensions/QueryableExtension.cs index e5c2152..3723d6f 100644 --- a/src/Extensions/QueryableExtension.cs +++ b/src/Extensions/QueryableExtension.cs @@ -84,7 +84,7 @@ public static (IQueryable, int?) Apply(this IQueryable queryable, Query qu int? count = null; // Count - if (query.Count) + if (query.Count ?? false) { count = result.Count(); } @@ -104,13 +104,13 @@ public static (IQueryable, int?) Apply(this IQueryable queryable, Query qu // Skip if (query.Skip > 0) { - result = result.Skip(query.Skip); + result = result.Skip(query.Skip ?? 0); } // Top if (query.Top > 0) { - result = result.Take(query.Top); + result = result.Take(query.Top ?? 0); } return (result, count); diff --git a/src/Query.cs b/src/Query.cs index c745783..f31d11d 100644 --- a/src/Query.cs +++ b/src/Query.cs @@ -1,10 +1,10 @@ public record Query { - public int Top { get; set; } - public int Skip { get; set; } - public bool Count { get; set; } - public string OrderBy { get; set; } = string.Empty; - public string Select { get; set; } = string.Empty; - public string Search { get; set; } = string.Empty; - public string Filter { get; set; } = string.Empty; + public int? Top { get; set; } + public int? Skip { get; set; } + public bool? Count { get; set; } + public string? OrderBy { get; set; } = string.Empty; + public string? Select { get; set; } = string.Empty; + public string? Search { get; set; } = string.Empty; + public string? Filter { get; set; } = string.Empty; } \ No newline at end of file diff --git a/tests/Tests.cs b/tests/Tests.cs index 25d1444..1f03fb4 100644 --- a/tests/Tests.cs +++ b/tests/Tests.cs @@ -40,7 +40,7 @@ public void Test_QueryWithTop() var (result, _) = _context.Users.AsQueryable().Apply(query); var sql = result.ToQueryString(); - var expectedSql = _context.Users.AsQueryable().Take(query.Top).ToQueryString(); + var expectedSql = _context.Users.AsQueryable().Take(query.Top ?? 0).ToQueryString(); Assert.Equal(expectedSql, sql); } @@ -64,7 +64,7 @@ public void Test_QueryWithSkip() var (result, _) = _context.Users.AsQueryable().Apply(query); var sql = result.ToQueryString(); - var expectedSql = _context.Users.AsQueryable().Skip(query.Skip).ToQueryString(); + var expectedSql = _context.Users.AsQueryable().Skip(query.Skip ?? 0).ToQueryString(); Assert.Equal(expectedSql, sql); }