Эх сурвалжийг харах

New internal request / query routing mechanism

Lukas Angerer 3 жил өмнө
parent
commit
1f73f49365

+ 1 - 0
src/RunnersMeet.Server/AppServer.cs

@@ -19,6 +19,7 @@ public class AppServer
 		new SpaRoutingModule(),
 		new ApiControllersModule(),
 		new SwaggerModule(),
+		new RequestHandlerModule(),
 		new PersistenceModule(),
 		new DomainServicesModule(),
 	};

+ 16 - 16
src/RunnersMeet.Server/Controllers/TracksController.cs

@@ -16,37 +16,37 @@ public class TracksController : ControllerBase
 	private readonly IFileStorage _fileStorage;
 	private readonly GpxParser _gpxParser;
 	private readonly QueryFactory _queryFactory;
+	private readonly IRequestRouter _requestRouter;
 	private readonly ApiSettings _settings;
 
-	public TracksController(IFileStorage fileStorage, GpxParser gpxParser, QueryFactory queryFactory, IOptions<ApiSettings> apiOptions)
+	public TracksController(
+		IFileStorage fileStorage,
+		GpxParser gpxParser,
+		QueryFactory queryFactory,
+		IOptions<ApiSettings> apiOptions,
+		IRequestRouter requestRouter)
 	{
 		_fileStorage = fileStorage;
 		_gpxParser = gpxParser;
 		_queryFactory = queryFactory;
+		_requestRouter = requestRouter;
 		_settings = apiOptions.Value;
 	}
 
 	[HttpGet]
-	public ActionResult<ResultPage<Track>> GetTracks([FromQuery] int? page, [FromQuery] string? owner, [FromQuery] string? filter)
+	public ActionResult<ResultPage<Track>> GetTracks([FromQuery] TracksRequest tracksRequest)
 	{
-		var query = _queryFactory.TracksQuery();
-		if (owner != null)
+		if (tracksRequest.Owner is "me")
 		{
-			if (owner == "me")
-			{
-				owner = ApiUser.Current.UserId;
-			}
-			query.ForOwner(owner);
+			tracksRequest = tracksRequest with { Owner = ApiUser.Current.UserId };
 		}
 
-		if (filter != null)
-		{
-			query.FilterByName(filter);
-		}
-
-		query.Paging((page ?? 0) * _settings.PageSize, _settings.PageSize);
+		var tracks = _requestRouter
+			.For(tracksRequest)
+			.With(new QueryPagingConfig(PageSize: _settings.PageSize))
+			.Process<IList<Track>>();
 
-		return Ok(new ResultPage<Track>(query.Get(), page ?? 0, _settings.PageSize));
+		return Ok(new ResultPage<Track>(tracks, tracksRequest.Page, _settings.PageSize));
 	}
 
 	[HttpPost]

+ 8 - 0
src/RunnersMeet.Server/Domain/TracksRequest.cs

@@ -0,0 +1,8 @@
+namespace RunnersMeet.Server.Domain;
+
+public sealed record TracksRequest
+{
+	public int Page { get; init; }
+	public string? Owner { get; init; }
+	public string? Filter { get; init; }
+}

+ 12 - 0
src/RunnersMeet.Server/IRequestHandler.cs

@@ -0,0 +1,12 @@
+namespace RunnersMeet.Server;
+
+public interface IRequestHandler<in TRequest, out TResponse>
+{
+	TResponse Handle(TRequest request);
+}
+
+public interface IRequestHandler<in TRequest, in TContext, out TResponse>
+	: IRequestHandler<TRequest, TResponse>
+{
+	IRequestHandler<TRequest, TResponse> Context(TContext context);
+}

+ 20 - 0
src/RunnersMeet.Server/IRequestRouter.cs

@@ -0,0 +1,20 @@
+namespace RunnersMeet.Server;
+
+public interface IRequestRouter
+{
+	IHandlerBuilder<TRequest> For<TRequest>(TRequest request);
+}
+
+public interface IProcessable
+{
+	TResponse Process<TResponse>();
+}
+
+public interface IHandlerBuilder<TRequest> : IProcessable
+{
+	IHandlerBuilderWithContext<TRequest, TContext> With<TContext>(TContext context);
+}
+
+public interface IHandlerBuilderWithContext<TRequest, TContext> : IProcessable
+{
+}

+ 17 - 0
src/RunnersMeet.Server/Persistence/LiteQueryableExtensions.cs

@@ -0,0 +1,17 @@
+using System.Linq.Expressions;
+using LiteDB;
+
+namespace RunnersMeet.Server.Persistence;
+
+public static class LiteQueryableExtensions
+{
+	public static ILiteQueryable<T> WhereIf<T>(this ILiteQueryable<T> queryable, bool condition, Expression<Func<T, bool>> predicate)
+	{
+		if (condition)
+		{
+			return queryable.Where(predicate);
+		}
+
+		return queryable;
+	}
+}

+ 1 - 0
src/RunnersMeet.Server/Persistence/PersistenceModule.cs

@@ -9,6 +9,7 @@ public class PersistenceModule : IAppConfigurationModule
 
 		services.AddSingleton<IDatabase, Database>();
 		services.AddSingleton<IFileStorage, FileStorage>();
+
 		services.AddScoped<QueryFactory, QueryFactory>();
 	}
 

+ 0 - 5
src/RunnersMeet.Server/Persistence/QueryFactory.cs

@@ -9,11 +9,6 @@ public class QueryFactory
 		_database = database;
 	}
 
-	public TracksQuery TracksQuery()
-	{
-		return new TracksQuery(_database);
-	}
-
 	public CreateTrackCommand CreateTrackCommand()
 	{
 		return new CreateTrackCommand(_database);

+ 3 - 0
src/RunnersMeet.Server/Persistence/QueryPagingConfig.cs

@@ -0,0 +1,3 @@
+namespace RunnersMeet.Server.Persistence;
+
+public sealed record QueryPagingConfig(int PageSize = 10);

+ 13 - 19
src/RunnersMeet.Server/Persistence/TracksQuery.cs

@@ -1,39 +1,33 @@
-using LiteDB;
 using RunnersMeet.Server.Domain;
 
 namespace RunnersMeet.Server.Persistence;
 
-public class TracksQuery : ITracksQuery
+public class TracksQuery : IRequestHandler<TracksRequest, QueryPagingConfig, IList<Track>>
 {
 	private readonly IDatabase _database;
-	private readonly ILiteQueryable<Track> _query;
+	private QueryPagingConfig _config;
 
 	public TracksQuery(IDatabase database)
 	{
 		_database = database;
-		_query = _database.Tracks.Query();
 	}
 
-	public TracksQuery ForOwner(string ownerId)
+	public IRequestHandler<TracksRequest, IList<Track>> Context(QueryPagingConfig context)
 	{
-		_query.Where(track => track.OwnerId == ownerId);
+		_config = context;
 		return this;
 	}
 
-	public TracksQuery FilterByName(string value)
+	public IList<Track> Handle(TracksRequest request)
 	{
-		_query.Where(track => track.DisplayName.Contains(value));
-		return this;
-	}
+		var query = _database.Tracks.Query();
+		query.WhereIf(request.Owner != null, track => track.OwnerId == request.Owner);
+		query.WhereIf(request.Filter != null, track => track.DisplayName.Contains(request.Filter!));
 
-	public TracksQuery Paging(int offset, int limit)
-	{
-		_query.OrderBy(track => track.DisplayName).Offset(offset).Limit(limit);
-		return this;
-	}
-
-	public IList<Track> Get()
-	{
-		return _query.ToList();
+		return query
+			.OrderBy(track => track.DisplayName)
+			.Offset(request.Page * _config.PageSize)
+			.Limit(_config.PageSize)
+			.ToList();
 	}
 }

+ 60 - 0
src/RunnersMeet.Server/RequestHandlerModule.cs

@@ -0,0 +1,60 @@
+using System.Reflection;
+
+namespace RunnersMeet.Server;
+
+public class RequestHandlerModule : IAppConfigurationModule
+{
+	public void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
+	{
+		services.AddScoped<IRequestRouter, RequestRouter>();
+
+		Register(services, new List<HandlerMatcher>
+		{
+			// More specific first, then fall back to more general
+			new HandlerMatcher(typeof(IRequestHandler<,,>)),
+			new HandlerMatcher(typeof(IRequestHandler<,>)),
+		});
+	}
+
+	public void ConfigureApplication(WebApplication app)
+	{
+	}
+
+	private void Register(IServiceCollection services, IList<HandlerMatcher> matchers)
+	{
+		foreach (var candidate in Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass))
+		{
+			var matcher = matchers.FirstOrDefault(m => m.IsMatch(candidate));
+			if (matcher != null)
+			{
+				services.AddScoped(matcher.GetHandlerInterface(candidate), candidate);
+			}
+		}
+	}
+
+	private sealed class HandlerMatcher
+	{
+		private readonly Type _genericInterfaceType;
+
+		public HandlerMatcher(Type genericInterfaceType)
+		{
+			_genericInterfaceType = genericInterfaceType;
+		}
+
+		public bool IsMatch(Type t) => t.GetInterfaces().Any(i => ImplementsExactly(i, _genericInterfaceType));
+
+		public Type GetHandlerInterface(Type t) => t.GetInterfaces().Single(i => ImplementsExactly(i, _genericInterfaceType));
+
+		private static bool ImplementsExactly(Type candidate, Type genericInterface)
+		{
+			if (!candidate.IsGenericType)
+			{
+				return false;
+			}
+
+			var genericTypeDefinition = candidate.GetGenericTypeDefinition();
+			return /*genericTypeDefinition.GenericTypeArguments.Length == genericInterface.GenericTypeArguments.Length
+			       &&*/ genericTypeDefinition == genericInterface;
+		}
+	}
+}

+ 60 - 0
src/RunnersMeet.Server/RequestRouter.cs

@@ -0,0 +1,60 @@
+namespace RunnersMeet.Server;
+
+public class RequestRouter : IRequestRouter
+{
+	private readonly IServiceProvider _serviceProvider;
+
+	public RequestRouter(IServiceProvider serviceProvider)
+	{
+		_serviceProvider = serviceProvider;
+	}
+
+	public IHandlerBuilder<TRequest> For<TRequest>(TRequest request)
+	{
+		return new HandlerBuilder<TRequest>(_serviceProvider, request);
+	}
+
+	private class HandlerBuilder<TRequest> : IHandlerBuilder<TRequest>
+	{
+		private readonly IServiceProvider _serviceProvider;
+		private readonly TRequest _request;
+
+		public HandlerBuilder(IServiceProvider serviceProvider, TRequest request)
+		{
+			_serviceProvider = serviceProvider;
+			_request = request;
+		}
+
+		public IHandlerBuilderWithContext<TRequest, TContext> With<TContext>(TContext context)
+		{
+			return new HandlerBuilderWithContext<TRequest, TContext>(_serviceProvider, _request, context);
+		}
+
+		public TResponse Process<TResponse>()
+		{
+			return _serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
+				.Handle(_request);
+		}
+	}
+
+	private class HandlerBuilderWithContext<TRequest, TContext> : IHandlerBuilderWithContext<TRequest, TContext>
+	{
+		private readonly IServiceProvider _serviceProvider;
+		private readonly TRequest _request;
+		private readonly TContext _context;
+
+		public HandlerBuilderWithContext(IServiceProvider serviceProvider, TRequest request, TContext context)
+		{
+			_serviceProvider = serviceProvider;
+			_request = request;
+			_context = context;
+		}
+
+		public TResponse Process<TResponse>()
+		{
+			return _serviceProvider.GetRequiredService<IRequestHandler<TRequest, TContext, TResponse>>()
+				.Context(_context)
+				.Handle(_request);
+		}
+	}
+}