├── assets ├── logo.png ├── social-banner.png └── simple-ednpoints-folderstructure.png ├── src ├── SimpleEndPoints │ ├── Assembly.cs │ ├── Core │ │ ├── HttpVerb.cs │ │ ├── SimpleEndpointAttribute.cs │ │ ├── SimpleEndpointBase.cs │ │ └── Endpoint.cs │ ├── Enrichers │ │ ├── IEndpointMetadataEnricher.cs │ │ ├── HttpMethodEndpointMetadataEnricher.cs │ │ └── RouteEndpointMetadataEnricher.cs │ ├── SimpleEndpoints.csproj │ ├── VerbScoped │ │ ├── PostEndpoint.cs │ │ ├── PutEndpoint.cs │ │ ├── DeleteEndpoint.cs │ │ └── GetEndpoint.cs │ ├── Routing │ │ ├── EndpointRoutingConvention.cs │ │ └── EndpointApiDescriptionProvider.cs │ ├── Extensions │ │ └── ServiceCollectionExtensions.cs │ └── SimpleEndpointsConfiguration.cs ├── SimpleEndpoints.Example │ ├── Endpoints │ │ ├── SimpleMessage │ │ │ ├── SimpleResponse.cs │ │ │ ├── SimpleMessage.cs │ │ │ └── SimpleMessageEndpoint.cs │ │ ├── WeatherForecast │ │ │ ├── WeatherForecast.cs │ │ │ └── WeatherForecastEndpoint.cs │ │ └── Greeting │ │ │ └── GreetingEndpoint.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── SimpleEndpoints.Example.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── Startup.cs ├── SimpleEndpoints.Tests │ ├── Shared │ │ └── WebAppFactory.cs │ ├── SimpleEndpoints.Tests.csproj │ ├── Routing │ │ ├── EndpointRoutingConventionShould.cs │ │ ├── RouteMetadataEnricherShould.cs │ │ └── HttpMethodMetadataEnricherShould.cs │ └── Endpoints │ │ └── Basic │ │ └── GreetingEndpointTestsShould.cs └── SimpleEndpoints.sln ├── .github └── workflows │ └── build.yml ├── LICENSE.md ├── .gitattributes ├── README.md └── .gitignore /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasiths/SimpleEndpoints/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/social-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasiths/SimpleEndpoints/HEAD/assets/social-banner.png -------------------------------------------------------------------------------- /src/SimpleEndPoints/Assembly.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("SimpleEndpoints.Tests")] -------------------------------------------------------------------------------- /assets/simple-ednpoints-folderstructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasiths/SimpleEndpoints/HEAD/assets/simple-ednpoints-folderstructure.png -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/SimpleMessage/SimpleResponse.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleEndpoints.Example.Endpoints.SimpleMessage 2 | { 3 | public class SimpleResponse 4 | { 5 | public string Message { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/SimpleMessage/SimpleMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace SimpleEndpoints.Example.Endpoints.SimpleMessage 3 | { 4 | public class SimpleMessage 5 | { 6 | public string Message { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Trace", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Trace" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/Shared/WebAppFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using SimpleEndpoints.Example; 3 | 4 | namespace SimpleEndpoints.Tests.Shared 5 | { 6 | public class WebAppFactory : WebApplicationFactory 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Core/HttpVerb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleEndpoints.Core 4 | { 5 | [Flags] 6 | public enum HttpVerb 7 | { 8 | Get = 1 << 0, 9 | Post = 1 << 1, 10 | Put = 1 << 2, 11 | Delete = 1 << 3, 12 | Head = 1 << 4, 13 | Patch = 1 << 5, 14 | Options = 1 << 6 15 | } 16 | } -------------------------------------------------------------------------------- /src/SimpleEndPoints/Core/SimpleEndpointAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleEndpoints.Core 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public sealed class SimpleEndpointAttribute: Attribute 7 | { 8 | public readonly string HttpVerb; 9 | 10 | public SimpleEndpointAttribute(HttpVerb httpVerb) 11 | { 12 | HttpVerb = httpVerb.ToString().ToUpper(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/WeatherForecast/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleEndpoints.Example.Endpoints.WeatherForecast 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Core/SimpleEndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace SimpleEndpoints.Core 4 | { 5 | [ApiController] 6 | [Route(EndpointRoute)] 7 | public abstract class SimpleEndpointBase : ControllerBase 8 | { 9 | public const string EndpointRouteToken = "[endpoint]"; 10 | public const string EndpointPrefixRouteToken = "[prefix]"; 11 | public const string EndpointRoute = EndpointPrefixRouteToken + EndpointRouteToken; 12 | } 13 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/SimpleEndpoints.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace SimpleEndpoints.Example 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => 17 | { 18 | webBuilder.UseStartup(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/SimpleMessage/SimpleMessageEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.VerbScoped; 5 | 6 | namespace SimpleEndpoints.Example.Endpoints.SimpleMessage 7 | { 8 | public class SimpleMessageEndpoint : AsyncPostEndpoint 9 | { 10 | // Here we force it to use a custom Route 11 | [Route("custom")] 12 | public override async Task> HandleAsync(SimpleMessage requestModel, CancellationToken cancellationToken = default) 13 | { 14 | return await Task.FromResult(new SimpleResponse {Message = "Hello " + requestModel.Message}); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:51765", 7 | "sslPort": 44399 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "EndpointTest": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/v1/weatherforecast", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/SimpleEndPoints/Enrichers/IEndpointMetadataEnricher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 3 | 4 | namespace SimpleEndpoints.Enrichers 5 | { 6 | public interface IEndpointMetadataEnricher 7 | { 8 | /// 9 | /// Enrich the with metadata 10 | /// 11 | /// to enrich 12 | /// The action to invoke the next in the pipeline 13 | void Enrich(ControllerModel controller, Action next); 14 | 15 | /// 16 | /// A enricher with a lower numeric value of 17 | /// will have its method called after that of a provider 18 | /// with a higher numeric value of 19 | /// 20 | int Order { get; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/SimpleEndpoints.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/SimpleEndpoints.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | https://github.com/dasiths/SimpleEndpoints 6 | A simple, convention-based, endpoint per action pattern implementation for AspNetCore 3.0+ 7 | MIT 8 | 2020 Dasith Wijesiriwardena 9 | true 10 | https://github.com/dasiths/SimpleEndpoints 11 | Git 12 | aspnetcore api controller endpoints 13 | 1.5.1 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.101 20 | - name: Build with dotnet 21 | run: dotnet build --configuration Release 22 | working-directory: ./src 23 | - name: Test with dotnet 24 | run: dotnet test 25 | working-directory: ./src 26 | - name: Create the SimpleEndPoints package 27 | if: github.ref == 'refs/heads/master' 28 | run: dotnet pack --configuration Release SimpleEndPoints 29 | working-directory: ./src 30 | - name: Publish the SimpleEndpoints package to NUGET.ORG 31 | if: github.ref == 'refs/heads/master' 32 | run: dotnet nuget push SimpleEndPoints/bin/Release/*.nupkg -k ${{secrets.NUGET_APIKEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate 33 | working-directory: ./src 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dasith Wijesiriwardena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/SimpleEndPoints/VerbScoped/PostEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.Core; 5 | 6 | namespace SimpleEndpoints.VerbScoped 7 | { 8 | 9 | [SimpleEndpoint(HttpVerb.Post)] 10 | public abstract class AsyncPostEndpoint : AsyncEndpointWithRequest 11 | { 12 | public abstract override Task HandleAsync(TRequest model, CancellationToken cancellationToken = default); 13 | } 14 | 15 | [SimpleEndpoint(HttpVerb.Post)] 16 | public abstract class AsyncPostEndpoint : AsyncEndpoint 17 | { 18 | public abstract override Task> HandleAsync(TRequest model, 19 | CancellationToken cancellationToken = default); 20 | } 21 | 22 | [SimpleEndpoint(HttpVerb.Post)] 23 | public abstract class PostEndpoint : EndpointWithRequest 24 | { 25 | public abstract override IActionResult Handle(TRequest model); 26 | } 27 | 28 | [SimpleEndpoint(HttpVerb.Post)] 29 | public abstract class PostEndpoint : Endpoint 30 | { 31 | public abstract override ActionResult Handle(TRequest model); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/VerbScoped/PutEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.Core; 5 | 6 | namespace SimpleEndpoints.VerbScoped 7 | { 8 | 9 | [SimpleEndpoint(HttpVerb.Put)] 10 | public abstract class AsyncPutEndpoint : AsyncEndpointWithRequest 11 | { 12 | public abstract override Task HandleAsync(TRequest model, 13 | CancellationToken cancellationToken = default); 14 | } 15 | 16 | [SimpleEndpoint(HttpVerb.Put)] 17 | public abstract class AsyncPutEndpoint : AsyncEndpoint 18 | { 19 | public abstract override Task> HandleAsync(TRequest model, 20 | CancellationToken cancellationToken = default); 21 | } 22 | 23 | [SimpleEndpoint(HttpVerb.Put)] 24 | public abstract class PutEndpoint : EndpointWithRequest 25 | { 26 | public abstract override IActionResult Handle(TRequest model); 27 | } 28 | 29 | [SimpleEndpoint(HttpVerb.Put)] 30 | public abstract class PutEndpoint : Endpoint 31 | { 32 | public abstract override ActionResult Handle(TRequest model); 33 | } 34 | } -------------------------------------------------------------------------------- /src/SimpleEndPoints/VerbScoped/DeleteEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.Core; 5 | 6 | namespace SimpleEndpoints.VerbScoped 7 | { 8 | [SimpleEndpoint(HttpVerb.Delete)] 9 | public abstract class AsyncDeleteEndpoint : AsyncEndpointWithRequest 10 | { 11 | public abstract override Task HandleAsync(TRequest model, 12 | CancellationToken cancellationToken = default); 13 | } 14 | 15 | [SimpleEndpoint(HttpVerb.Delete)] 16 | public abstract class AsyncDeleteEndpoint : AsyncEndpoint 17 | { 18 | public abstract override Task> HandleAsync(TRequest model, 19 | CancellationToken cancellationToken = default); 20 | } 21 | 22 | [SimpleEndpoint(HttpVerb.Delete)] 23 | public abstract class DeleteEndpoint : EndpointWithRequest 24 | { 25 | public abstract override IActionResult Handle(TRequest model); 26 | } 27 | 28 | [SimpleEndpoint(HttpVerb.Delete)] 29 | public abstract class DeleteEndpoint : Endpoint 30 | { 31 | public abstract override ActionResult Handle(TRequest model); 32 | } 33 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.OpenApi.Models; 6 | using SimpleEndpoints.Extensions; 7 | 8 | namespace SimpleEndpoints.Example 9 | { 10 | public class Startup 11 | { 12 | public void ConfigureServices(IServiceCollection services) 13 | { 14 | var tokenDictionary = new Dictionary() 15 | { 16 | {"assembly-name", Assembly.GetExecutingAssembly().GetName(true).Name} 17 | }; 18 | 19 | services.AddControllers(); 20 | services.AddSimpleEndpointsRouting(options => options 21 | .WithRouteTokens(tokenDictionary) 22 | .WithRoutePrefix("api/v1") 23 | ); 24 | 25 | services.AddSwaggerGen(options => 26 | options.SwaggerDoc("v1", new OpenApiInfo { Title = "Simple endpoints", Version = "v1" })); 27 | } 28 | 29 | public void Configure(IApplicationBuilder app) 30 | { 31 | app.UseRouting(); 32 | app.UseEndpoints(options => options.MapControllers()); 33 | 34 | app.UseSwagger(); 35 | app.UseSwaggerUI(options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "Simple endpoints V1")); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/WeatherForecast/WeatherForecastEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using SimpleEndpoints.VerbScoped; 9 | 10 | namespace SimpleEndpoints.Example.Endpoints.WeatherForecast 11 | { 12 | public class WeatherForecastEndpoint : AsyncGetEndpoint> 13 | { 14 | private readonly ILogger _logger; 15 | 16 | private static readonly string[] Summaries = new[] 17 | { 18 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 19 | }; 20 | 21 | public WeatherForecastEndpoint(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public override async Task>> HandleAsync(CancellationToken cancellationToken = default) 27 | { 28 | _logger.LogDebug("Request received"); 29 | 30 | var rng = new Random(); 31 | return await Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast 32 | { 33 | Date = DateTime.Now.AddDays(index), 34 | TemperatureC = rng.Next(-20, 55), 35 | Summary = Summaries[rng.Next(Summaries.Length)] 36 | }).ToList()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Core/Endpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace SimpleEndpoints.Core 6 | { 7 | public abstract class AsyncEndpoint : SimpleEndpointBase 8 | { 9 | public abstract Task HandleAsync(CancellationToken cancellationToken = default); 10 | } 11 | 12 | public abstract class AsyncEndpointWithRequest : SimpleEndpointBase 13 | { 14 | public abstract Task HandleAsync(TRequest requestModel, CancellationToken cancellationToken = default); 15 | } 16 | 17 | public abstract class AsyncEndpoint : SimpleEndpointBase 18 | { 19 | public abstract Task> HandleAsync(CancellationToken cancellationToken = default); 20 | } 21 | 22 | public abstract class AsyncEndpoint : SimpleEndpointBase 23 | { 24 | public abstract Task> HandleAsync(TRequest requestModel, CancellationToken cancellationToken = default); 25 | } 26 | 27 | public abstract class Endpoint : SimpleEndpointBase 28 | { 29 | public abstract IActionResult Handle(); 30 | } 31 | 32 | public abstract class EndpointWithRequest : SimpleEndpointBase 33 | { 34 | public abstract IActionResult Handle(TRequest requestModel); 35 | } 36 | 37 | public abstract class Endpoint : SimpleEndpointBase 38 | { 39 | public abstract ActionResult Handle(); 40 | } 41 | 42 | public abstract class Endpoint : SimpleEndpointBase 43 | { 44 | public abstract ActionResult Handle(TRequest requestModel); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Routing/EndpointRoutingConvention.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 5 | using Microsoft.Extensions.Logging; 6 | using SimpleEndpoints.Enrichers; 7 | 8 | namespace SimpleEndpoints.Routing 9 | { 10 | public sealed class EndpointRoutingConvention : IApplicationModelConvention 11 | { 12 | private readonly IEnumerable _metadataEnrichers; 13 | private readonly ILogger _logger; 14 | 15 | public EndpointRoutingConvention(IEnumerable metadataEnrichers, ILogger logger) 16 | { 17 | _metadataEnrichers = metadataEnrichers; 18 | _logger = logger; 19 | } 20 | 21 | public void Apply(ApplicationModel application) 22 | { 23 | void LastAction(ControllerModel c) 24 | { 25 | _logger.LogTrace("Enricher Pipeline end reached"); 26 | } 27 | 28 | var apply = _metadataEnrichers.OrderBy(e => e.Order) 29 | .Aggregate((Action) LastAction, (action, enricher) => c => 30 | { 31 | _logger.LogTrace($"Enriching started using: {enricher.GetType().Name} with Order: {enricher.Order}"); 32 | enricher.Enrich(c, action); 33 | _logger.LogTrace($"Enriching completed with: {enricher.GetType().Name}"); 34 | }); 35 | 36 | foreach (var controller in application.Controllers) 37 | { 38 | _logger.LogTrace($"Applying enricher pipeline for {controller.ControllerName}"); 39 | apply(controller); 40 | _logger.LogTrace($"Completed enricher pipeline for {controller.ControllerName}"); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 4 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Options; 7 | using SimpleEndpoints.Enrichers; 8 | using SimpleEndpoints.Routing; 9 | 10 | namespace SimpleEndpoints.Extensions 11 | { 12 | internal sealed class ConfigureMvcOptionsForSimpleEndpoints : IConfigureOptions 13 | { 14 | private readonly EndpointRoutingConvention _endpointRoutingConvention; 15 | 16 | public ConfigureMvcOptionsForSimpleEndpoints(EndpointRoutingConvention endpointRoutingConvention) 17 | { 18 | _endpointRoutingConvention = endpointRoutingConvention; 19 | } 20 | 21 | public void Configure(MvcOptions options) 22 | { 23 | options.Conventions.Add(_endpointRoutingConvention); 24 | } 25 | } 26 | 27 | public static class ServiceCollectionExtensions 28 | { 29 | public static IServiceCollection AddSimpleEndpointsRouting(this IServiceCollection services, 30 | Action configure = null) 31 | { 32 | services.AddEndpointMetadataEnricher(); 33 | services.AddEndpointMetadataEnricher(); 34 | 35 | services.AddSingleton(); 36 | services.AddSingleton(); 37 | services.AddSingleton, ConfigureMvcOptionsForSimpleEndpoints>(); 38 | 39 | if (configure != null) 40 | { 41 | services.Configure(configure); 42 | } 43 | 44 | return services; 45 | } 46 | 47 | public static IServiceCollection AddEndpointMetadataEnricher(this IServiceCollection services) where T : class, IEndpointMetadataEnricher 48 | { 49 | services.AddSingleton(); 50 | return services; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29326.143 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEndpoints.Example", "SimpleEndpoints.Example\SimpleEndpoints.Example.csproj", "{F8121641-4AD6-45E4-957C-73824EEEBB08}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEndpoints", "SimpleEndPoints\SimpleEndpoints.csproj", "{06D8DE86-AC08-452C-882D-6F57CFF4835E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleEndpoints.Tests", "SimpleEndpoints.Tests\SimpleEndpoints.Tests.csproj", "{60D80A29-1FFD-495A-9CF0-9285920F0356}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {F8121641-4AD6-45E4-957C-73824EEEBB08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {F8121641-4AD6-45E4-957C-73824EEEBB08}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {F8121641-4AD6-45E4-957C-73824EEEBB08}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {F8121641-4AD6-45E4-957C-73824EEEBB08}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {06D8DE86-AC08-452C-882D-6F57CFF4835E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {06D8DE86-AC08-452C-882D-6F57CFF4835E}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {06D8DE86-AC08-452C-882D-6F57CFF4835E}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {06D8DE86-AC08-452C-882D-6F57CFF4835E}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {60D80A29-1FFD-495A-9CF0-9285920F0356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {60D80A29-1FFD-495A-9CF0-9285920F0356}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {60D80A29-1FFD-495A-9CF0-9285920F0356}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {60D80A29-1FFD-495A-9CF0-9285920F0356}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {931BC16E-291D-4ED1-9FA3-ED9D0BDFEF1C} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/VerbScoped/GetEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.Core; 5 | 6 | namespace SimpleEndpoints.VerbScoped 7 | { 8 | [SimpleEndpoint(HttpVerb.Get)] 9 | public abstract class AsyncGetEndpoint : AsyncEndpoint 10 | { 11 | public abstract override Task HandleAsync(CancellationToken cancellationToken = default); 12 | } 13 | 14 | [SimpleEndpoint(HttpVerb.Get)] 15 | public abstract class AsyncGetEndpointWithRequest : AsyncEndpointWithRequest 16 | { 17 | public abstract override Task HandleAsync([FromQuery] TRequest model, 18 | CancellationToken cancellationToken = default); 19 | } 20 | 21 | [SimpleEndpoint(HttpVerb.Get)] 22 | public abstract class AsyncGetEndpoint : AsyncEndpoint 23 | { 24 | public abstract override Task> HandleAsync( 25 | CancellationToken cancellationToken = default); 26 | } 27 | 28 | [SimpleEndpoint(HttpVerb.Get)] 29 | public abstract class AsyncGetEndpoint : AsyncEndpoint 30 | { 31 | public abstract override Task> HandleAsync([FromQuery] TRequest model, 32 | CancellationToken cancellationToken = default); 33 | } 34 | 35 | [SimpleEndpoint(HttpVerb.Get)] 36 | public abstract class GetEndpoint : Endpoint 37 | { 38 | public abstract override IActionResult Handle(); 39 | } 40 | 41 | [SimpleEndpoint(HttpVerb.Get)] 42 | public abstract class GetEndpoint : Endpoint 43 | { 44 | public abstract override ActionResult Handle(); 45 | } 46 | 47 | [SimpleEndpoint(HttpVerb.Get)] 48 | public abstract class GetEndpointWithRequest : EndpointWithRequest 49 | { 50 | public abstract override IActionResult Handle([FromQuery] TRequest model); 51 | } 52 | 53 | [SimpleEndpoint(HttpVerb.Get)] 54 | public abstract class GetEndpoint : Endpoint 55 | { 56 | public abstract override ActionResult Handle([FromQuery] TRequest model); 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Routing/EndpointApiDescriptionProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 3 | using Microsoft.AspNetCore.Mvc.Controllers; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | using SimpleEndpoints.Core; 7 | 8 | namespace SimpleEndpoints.Routing 9 | { 10 | public class EndpointApiDescriptionProvider : IApiDescriptionProvider 11 | { 12 | private readonly ILogger _logger; 13 | private readonly SimpleEndpointsConfiguration _simpleEndpointsConfiguration; 14 | 15 | public EndpointApiDescriptionProvider(IOptions simpleEndpointsConfiguration, ILogger logger) 16 | { 17 | _logger = logger; 18 | _simpleEndpointsConfiguration = simpleEndpointsConfiguration.Value; 19 | } 20 | 21 | 22 | public void OnProvidersExecuting(ApiDescriptionProviderContext context) 23 | { 24 | foreach (var apiDescription in context.Results) 25 | { 26 | if (apiDescription.ActionDescriptor is ControllerActionDescriptor controller) 27 | { 28 | _logger.LogTrace($"{controller.ControllerName}.{controller.ActionName}: {nameof(apiDescription.HttpMethod)} is" + 29 | $" {apiDescription.HttpMethod ?? "NULL"}"); 30 | 31 | if (apiDescription.HttpMethod is null) 32 | { 33 | if (controller.ControllerTypeInfo.GetCustomAttributes(typeof(SimpleEndpointAttribute), 34 | true).FirstOrDefault() is SimpleEndpointAttribute attribute) 35 | { 36 | _logger.LogTrace($"Setting {controller.ControllerName} with {nameof(apiDescription.HttpMethod)} of: {attribute.HttpVerb}"); 37 | apiDescription.HttpMethod = attribute.HttpVerb; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | public void OnProvidersExecuted(ApiDescriptionProviderContext context) 45 | { 46 | } 47 | 48 | public int Order => _simpleEndpointsConfiguration.EndpointApiDescriptionProviderOrder; 49 | } 50 | } -------------------------------------------------------------------------------- /src/SimpleEndPoints/Enrichers/HttpMethodEndpointMetadataEnricher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 4 | using Microsoft.AspNetCore.Routing; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | using SimpleEndpoints.Core; 8 | 9 | namespace SimpleEndpoints.Enrichers 10 | { 11 | public class HttpMethodEndpointMetadataEnricher : IEndpointMetadataEnricher 12 | { 13 | private readonly ILogger _logger; 14 | private readonly SimpleEndpointsConfiguration _simpleEndpointsConfiguration; 15 | 16 | public HttpMethodEndpointMetadataEnricher(IOptions simpleEndpointsConfiguration, ILogger logger) 17 | { 18 | _logger = logger; 19 | _simpleEndpointsConfiguration = simpleEndpointsConfiguration.Value; 20 | } 21 | 22 | public void Enrich(ControllerModel controller, Action next) 23 | { 24 | _logger.LogTrace($"Start processing {controller.ControllerName}"); 25 | _logger.LogTrace($"Inspecting for {nameof(SimpleEndpointAttribute)}"); 26 | if (controller.ControllerType 27 | .GetCustomAttributes(typeof(SimpleEndpointAttribute), true).FirstOrDefault() is SimpleEndpointAttribute attribute) 28 | { 29 | _logger.LogTrace($"{nameof(SimpleEndpointAttribute)} found"); 30 | if (controller.Selectors.Any()) 31 | { 32 | var hasExistingHttpMetadata = 33 | !controller.Selectors[0].EndpointMetadata.Any(m => m is HttpMethodMetadata); 34 | _logger.LogTrace($"{controller.ControllerName} has {nameof(HttpMethodMetadata)}: {hasExistingHttpMetadata}"); 35 | 36 | if (hasExistingHttpMetadata) 37 | { 38 | controller.Selectors[0].EndpointMetadata.Add(new HttpMethodMetadata(new[] { (attribute).HttpVerb })); 39 | _logger.LogTrace($"{controller.ControllerName} has been added {nameof(HttpMethodMetadata)} with {attribute.HttpVerb}"); 40 | } 41 | } 42 | } 43 | 44 | _logger.LogTrace($"End processing {controller.ControllerName}. Invoking Next enricher."); 45 | 46 | next(controller); 47 | } 48 | 49 | public int Order => _simpleEndpointsConfiguration.HttpMethodEndpointMetadataEnricherOrder; 50 | } 51 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/Routing/EndpointRoutingConventionShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 9 | using Microsoft.AspNetCore.Routing; 10 | using Microsoft.Extensions.Logging.Abstractions; 11 | using Microsoft.Extensions.Options; 12 | using Shouldly; 13 | using SimpleEndpoints.Enrichers; 14 | using SimpleEndpoints.Routing; 15 | using SimpleEndpoints.VerbScoped; 16 | using Xunit; 17 | 18 | namespace SimpleEndpoints.Tests.Routing 19 | { 20 | public class EndpointRoutingConventionShould 21 | { 22 | [Fact] 23 | public void MapConventions() 24 | { 25 | //Arrange 26 | var classAttributes = Attribute.GetCustomAttributes(typeof(TestEndpoint)); 27 | var conventions = new EndpointRoutingConvention(new List() 28 | { 29 | new RouteEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 30 | NullLogger.Instance), 31 | new HttpMethodEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 32 | NullLogger.Instance) 33 | }, NullLogger.Instance); 34 | 35 | var controller = new ControllerModel(typeof(TestEndpoint).GetTypeInfo(), classAttributes) 36 | { 37 | Selectors = 38 | { 39 | new SelectorModel 40 | { 41 | AttributeRouteModel = new AttributeRouteModel {Template = "route"}, 42 | EndpointMetadata = { } 43 | } 44 | }, 45 | ControllerName = nameof(TestEndpoint) 46 | }; 47 | 48 | //Act 49 | conventions.Apply(new ApplicationModel { Controllers = { controller } }); 50 | 51 | controller.Selectors[0].AttributeRouteModel.Template.ShouldBe("route"); 52 | 53 | //Assert 54 | controller.Selectors[0].EndpointMetadata.Count.ShouldBe(1); 55 | controller.Selectors[0].EndpointMetadata.First().ShouldBeOfType(); 56 | controller.Selectors[0].EndpointMetadata.OfType().First().HttpMethods 57 | .ShouldBe(new[] { "GET" }); 58 | } 59 | 60 | public class TestEndpoint : AsyncGetEndpoint 61 | { 62 | public override async Task HandleAsync(CancellationToken cancellationToken = default) 63 | { 64 | return await Task.FromResult(Ok(new { Success = true })); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/SimpleEndPoints/SimpleEndpointsConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using SimpleEndpoints.Core; 4 | 5 | namespace SimpleEndpoints 6 | { 7 | public class SimpleEndpointsConfiguration 8 | { 9 | public const int DefaultRouteEndpointMetadataEnricherOrder = 100; 10 | public const int DefaultHttpMethodEndpointMetadataEnricherOrder = 100; 11 | private const int DefaultEndpointApiDescriptionProviderOrder = 100; 12 | 13 | public string RoutePrefix { get; private set; } 14 | public string EndpointNamingConventionEnding { get; private set; } 15 | public int EndpointApiDescriptionProviderOrder { get; private set; } 16 | public int RouteEndpointMetadataEnricherOrder { get; private set; } 17 | public int HttpMethodEndpointMetadataEnricherOrder { get; private set; } 18 | public ReadOnlyDictionary RouteTokenDictionary { get; private set; } 19 | 20 | public SimpleEndpointsConfiguration() 21 | { 22 | RoutePrefix = string.Empty; 23 | EndpointNamingConventionEnding = nameof(Endpoint); 24 | RouteEndpointMetadataEnricherOrder = DefaultRouteEndpointMetadataEnricherOrder; 25 | HttpMethodEndpointMetadataEnricherOrder = DefaultHttpMethodEndpointMetadataEnricherOrder; 26 | EndpointApiDescriptionProviderOrder = DefaultEndpointApiDescriptionProviderOrder; 27 | RouteTokenDictionary = new ReadOnlyDictionary(new Dictionary()); 28 | } 29 | 30 | public SimpleEndpointsConfiguration WithRoutePrefix(string prefix) 31 | { 32 | RoutePrefix = prefix; 33 | return this; 34 | } 35 | 36 | public SimpleEndpointsConfiguration WithEndpointNamingConventionEndingWith(string endpointNamingConventionEnding) 37 | { 38 | EndpointNamingConventionEnding = endpointNamingConventionEnding; 39 | return this; 40 | } 41 | 42 | public SimpleEndpointsConfiguration WithRouteTokens(Dictionary tokens) 43 | { 44 | RouteTokenDictionary = new ReadOnlyDictionary(tokens); 45 | return this; 46 | } 47 | 48 | public SimpleEndpointsConfiguration WithEndpointApiDescriptionProviderOrder(int order) 49 | { 50 | EndpointApiDescriptionProviderOrder = order; 51 | return this; 52 | } 53 | 54 | public SimpleEndpointsConfiguration WithRouteEndpointMetadataEnricherOrder(int order) 55 | { 56 | RouteEndpointMetadataEnricherOrder = order; 57 | return this; 58 | } 59 | 60 | public SimpleEndpointsConfiguration WithHttpMethodEndpointMetadataEnricherOrder(int order) 61 | { 62 | HttpMethodEndpointMetadataEnricherOrder = order; 63 | return this; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/Routing/RouteMetadataEnricherShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 8 | using Microsoft.Extensions.Logging.Abstractions; 9 | using Microsoft.Extensions.Options; 10 | using Shouldly; 11 | using SimpleEndpoints.Enrichers; 12 | using SimpleEndpoints.VerbScoped; 13 | using Xunit; 14 | 15 | namespace SimpleEndpoints.Tests.Routing 16 | { 17 | public class RouteMetadataEnricherShould 18 | { 19 | [Fact] 20 | public void MapRoute_ReplacingPlaceholderWithEndpointName() 21 | { 22 | //Arrange 23 | var enricher = new RouteEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration().WithRoutePrefix("api")), 24 | NullLogger.Instance); 25 | var classAttributes = Attribute.GetCustomAttributes(typeof(TestEndpoint)); 26 | var controller = CreateControllerModel(classAttributes.OfType().First().Template); 27 | 28 | //Act 29 | enricher.Enrich(controller, c => { }); 30 | 31 | //Assert 32 | controller.Selectors[0].AttributeRouteModel.Template.ShouldBe("api/Test"); 33 | } 34 | 35 | [Fact] 36 | public void MapRoute_HonouringRouteAttribute() 37 | { 38 | //Arrange 39 | var enricher = new RouteEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 40 | NullLogger.Instance); 41 | var controller = CreateControllerModel("my-route"); 42 | 43 | //Act 44 | enricher.Enrich(controller, c => {}); 45 | 46 | //Assert 47 | controller.Selectors[0].AttributeRouteModel.Template.ShouldBe("my-route"); 48 | } 49 | 50 | private static ControllerModel CreateControllerModel(string routeTemplate) 51 | { 52 | var classAttributes = Attribute.GetCustomAttributes(typeof(TestEndpoint)); 53 | 54 | var controller = new ControllerModel(typeof(TestEndpoint).GetTypeInfo(), classAttributes) 55 | { 56 | Selectors = 57 | { 58 | new SelectorModel 59 | { 60 | AttributeRouteModel = new AttributeRouteModel {Template = routeTemplate}, 61 | EndpointMetadata = { } 62 | } 63 | }, 64 | ControllerName = nameof(TestEndpoint) 65 | }; 66 | return controller; 67 | } 68 | 69 | public class TestEndpoint : AsyncGetEndpoint 70 | { 71 | public override async Task HandleAsync(CancellationToken cancellationToken = default) 72 | { 73 | return await Task.FromResult(Ok(new {Success = true})); 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Example/Endpoints/Greeting/GreetingEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SimpleEndpoints.Core; 5 | using SimpleEndpoints.VerbScoped; 6 | 7 | namespace SimpleEndpoints.Example.Endpoints.Greeting 8 | { 9 | public class GreetingRequest 10 | { 11 | public string Name { get; set; } 12 | } 13 | 14 | public class GreetingResponse 15 | { 16 | public string Greeting { get; set; } 17 | } 18 | 19 | //As we are extending AsyncEndpoint we must specify the [HttpGet] attribute 20 | public class GreetingAsyncEndpoint : AsyncEndpoint 21 | { 22 | [HttpGet] 23 | public override async Task> HandleAsync([FromQuery] GreetingRequest requestModel, 24 | CancellationToken cancellationToken = default) 25 | { 26 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {requestModel.Name}") }); 27 | } 28 | } 29 | 30 | //As we now extend AsyncGetEndpoint we know that the action is a Get so it can be omitted 31 | public class GreetingAsyncGetEndpoint : AsyncGetEndpoint 32 | { 33 | public override async Task> HandleAsync([FromQuery] GreetingRequest requestModel, 34 | CancellationToken cancellationToken = default) 35 | { 36 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {requestModel.Name}") }); 37 | } 38 | } 39 | 40 | //We can also overwrite the route segement - /api/GreetingAsyncGetWithRoute/get 41 | public class GreetingAsyncGetWithRouteEndpoint : AsyncGetEndpoint 42 | { 43 | [Route("get")] 44 | public override async Task> HandleAsync([FromQuery] GreetingRequest requestModel, 45 | CancellationToken cancellationToken = default) 46 | { 47 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {requestModel.Name}") }); 48 | } 49 | } 50 | 51 | //Or we can also overwrite the route root segment - /api/greeting/get 52 | [Route("api/greeting")] 53 | public class GreetingAsyncGetWithRouteEndpoint2 : AsyncGetEndpoint 54 | { 55 | [Route("get")] 56 | public override async Task> HandleAsync([FromQuery] GreetingRequest requestModel, 57 | CancellationToken cancellationToken = default) 58 | { 59 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {requestModel.Name}") }); 60 | } 61 | } 62 | 63 | //We can extend AsyncGetEndpoint and [HttpPost], we now expect a POST 64 | [Route("api/greeting/[assembly-name]")] 65 | public class GreetingAsyncPostWithContradictoryRouteAndHttpMethodEndpoint : AsyncGetEndpoint 66 | { 67 | [HttpPost] 68 | public override async Task> HandleAsync([FromQuery] GreetingRequest requestModel, 69 | CancellationToken cancellationToken = default) 70 | { 71 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {requestModel.Name}") }); 72 | } 73 | } 74 | 75 | //Here we are using an endpoint constructed from extending the base class 76 | [SimpleEndpoint(HttpVerb.Get)] 77 | [Route("[prefix]custom/[endpoint]")] 78 | public class GreetingAsyncGetUsingCustomEndpoint : SimpleEndpointBase 79 | { 80 | [Route("custom-method")] 81 | public async Task> CustomHandleMethod([FromQuery] int id, [FromQuery] string name) 82 | { 83 | return Ok(new GreetingResponse { Greeting = await Task.FromResult($"Hello {name} with id {id}") }); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/SimpleEndPoints/Enrichers/RouteEndpointMetadataEnricher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | using SimpleEndpoints.Core; 8 | using SimpleEndpoints.Extensions; 9 | 10 | namespace SimpleEndpoints.Enrichers 11 | { 12 | public class RouteEndpointMetadataEnricher : IEndpointMetadataEnricher 13 | { 14 | private readonly ILogger _logger; 15 | private readonly SimpleEndpointsConfiguration _simpleEndpointsConfiguration; 16 | 17 | public RouteEndpointMetadataEnricher(IOptions simpleEndpointsConfiguration, ILogger logger) 18 | { 19 | _logger = logger; 20 | _simpleEndpointsConfiguration = simpleEndpointsConfiguration.Value; 21 | } 22 | 23 | public void Enrich(ControllerModel controller, Action next) 24 | { 25 | _logger.LogTrace($"Start processing {controller.ControllerName}"); 26 | var routeBuilder = new StringBuilder(); 27 | 28 | if (controller.Selectors.Any()) 29 | { 30 | var routeTemplate = controller.Selectors[0].AttributeRouteModel.Template; 31 | 32 | _logger.LogTrace($"RouteTemplate is {routeTemplate}"); 33 | 34 | routeBuilder.Append(routeTemplate); 35 | 36 | _logger.LogTrace($"Route prefix of \"{_simpleEndpointsConfiguration.RoutePrefix}/\" applied"); 37 | routeBuilder.Replace( 38 | $"{SimpleEndpointBase.EndpointPrefixRouteToken}", 39 | $"{_simpleEndpointsConfiguration.RoutePrefix}/"); 40 | 41 | var endsWithEndpointName = controller.ControllerName 42 | .EndsWith(_simpleEndpointsConfiguration.EndpointNamingConventionEnding, 43 | StringComparison.OrdinalIgnoreCase); 44 | 45 | _logger.LogTrace($"Endpoint name ends with \"{_simpleEndpointsConfiguration.EndpointNamingConventionEnding}\" = {endsWithEndpointName}"); 46 | 47 | if (endsWithEndpointName) 48 | { 49 | var controllerNameWithoutEndpoint = controller.ControllerName.Substring(0, 50 | controller.ControllerName.Length - _simpleEndpointsConfiguration.EndpointNamingConventionEnding.Length); 51 | 52 | if (string.IsNullOrWhiteSpace(controllerNameWithoutEndpoint)) 53 | { 54 | _logger.LogTrace($"Calculated name is empty. Reverting to full name."); 55 | controllerNameWithoutEndpoint = controller.ControllerName; 56 | } 57 | 58 | _logger.LogTrace($"Calculated endpoint name: {controllerNameWithoutEndpoint}"); 59 | routeBuilder.Replace( 60 | $"{SimpleEndpointBase.EndpointRouteToken}", controllerNameWithoutEndpoint); 61 | } 62 | else 63 | { 64 | _logger.LogTrace($"Calculated endpoint name: {controller.ControllerName}"); 65 | routeBuilder.Replace( 66 | $"{SimpleEndpointBase.EndpointRouteToken}", controller.ControllerName); 67 | } 68 | 69 | foreach (var keyValuePair in _simpleEndpointsConfiguration.RouteTokenDictionary) 70 | { 71 | _logger.LogTrace($"Replacing custom route token [{keyValuePair.Key}] with {keyValuePair.Value}"); 72 | routeBuilder.Replace($"[{keyValuePair.Key}]", keyValuePair.Value); 73 | } 74 | 75 | var newRoute = routeBuilder.ToString(); 76 | _logger.LogTrace($"Calculated route template: {newRoute}"); 77 | 78 | controller.Selectors[0].AttributeRouteModel.Template = newRoute; 79 | } 80 | 81 | _logger.LogTrace($"End processing {controller.ControllerName}. Invoking Next enricher."); 82 | 83 | next(controller); 84 | } 85 | 86 | public int Order => _simpleEndpointsConfiguration.RouteEndpointMetadataEnricherOrder; 87 | } 88 | } -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/Endpoints/Basic/GreetingEndpointTestsShould.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json; 4 | using Shouldly; 5 | using SimpleEndpoints.Example.Endpoints.Greeting; 6 | using SimpleEndpoints.Tests.Shared; 7 | using Xunit; 8 | 9 | namespace SimpleEndpoints.Tests.Endpoints.Basic 10 | { 11 | public class GreetingEndpointTestsShould 12 | { 13 | private readonly WebAppFactory _factory; 14 | private readonly string routePrefix = "api/v1/"; 15 | 16 | public GreetingEndpointTestsShould() 17 | { 18 | _factory = new WebAppFactory(); 19 | } 20 | 21 | [Fact] 22 | public async Task GreetingAsyncEndpoint() 23 | { 24 | // Arrange 25 | using var client = _factory.CreateClient(); 26 | 27 | // Act 28 | var response = await client.GetAsync($"{routePrefix}GreetingAsync?name=peterparker"); 29 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 30 | 31 | // Assert 32 | responseContent.Greeting.ShouldBe("Hello peterparker"); 33 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 34 | } 35 | 36 | [Fact] 37 | public async Task GreetingAsyncGetEndpoint() 38 | { 39 | // Arrange 40 | using var client = _factory.CreateClient(); 41 | 42 | // Act 43 | var response = await client.GetAsync($"{routePrefix}GreetingAsyncGet?name=peterparker"); 44 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 45 | 46 | // Assert 47 | responseContent.Greeting.ShouldBe("Hello peterparker"); 48 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 49 | } 50 | 51 | [Fact] 52 | public async Task GreetingAsyncGetWithRouteEndpoint() 53 | { 54 | // Arrange 55 | using var client = _factory.CreateClient(); 56 | 57 | // Act 58 | var response = await client.GetAsync($"{routePrefix}GreetingAsyncGetWithRoute/get?name=peterparker"); 59 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 60 | 61 | // Assert 62 | responseContent.Greeting.ShouldBe("Hello peterparker"); 63 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 64 | } 65 | 66 | [Fact] 67 | public async Task GreetingAsyncGetWithContradictoryRouteEndpoint() 68 | { 69 | // Arrange 70 | using var client = _factory.CreateClient(); 71 | 72 | // Act 73 | var response = await client.GetAsync("/api/greeting/get?name=peterparker"); 74 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 75 | 76 | // Assert 77 | responseContent.Greeting.ShouldBe("Hello peterparker"); 78 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 79 | } 80 | 81 | [Fact] 82 | public async Task GreetingAsyncPostWithContradictoryRouteAndHttpMethodEndpoint() 83 | { 84 | // Arrange 85 | using var client = _factory.CreateClient(); 86 | 87 | // Act 88 | var response = await client.PostAsync($"/api/greeting/{nameof(SimpleEndpoints)}.{nameof(Example)}?name=peterparker", new StringContent("")); 89 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 90 | 91 | // Assert 92 | responseContent.Greeting.ShouldBe("Hello peterparker"); 93 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 94 | } 95 | 96 | [Fact] 97 | public async Task GreetingAsyncGetUsingCustomEndpoint() 98 | { 99 | // Arrange 100 | using var client = _factory.CreateClient(); 101 | 102 | // Act 103 | var response = await client.GetAsync($"{routePrefix}custom/GreetingAsyncGetUsingCustom/custom-method?id=1&name=peterparker"); 104 | var responseContent = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); 105 | 106 | // Assert 107 | responseContent.Greeting.ShouldBe("Hello peterparker with id 1"); 108 | response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleEndpoints ![Build Master](https://github.com/dasiths/SimpleEndpoints/workflows/Build%20Master/badge.svg?branch=master) [![NuGet](https://img.shields.io/nuget/v/SimpleEndpoints.svg)](https://www.nuget.org/packages/SimpleEndpoints) [![Downloads](https://img.shields.io/nuget/dt/SimpleEndpoints.svg)](https://www.nuget.org/packages/SimpleEndpoints/) 2 | 3 | ### A simple, convention-based, endpoint per action pattern implementation for AspNetCore 3.0+ 4 | 5 | Logo 6 | 7 | ## Motivation 8 | 9 | The aim of this pattern is to get away from the bloated god controllers that have a million action methods and so many dependencies. By following the SimpleEndpoints pattern we keep the endpoint scoped to a small feature and lightweight which makes it easier to understand and manage. The aim is not to blur the line between the controllers and the domain layer. You can choose to dispatch the request to the domain from the endpoint or handle it in the endpoint itself. Make an informed choice based to the context. 10 | 11 | More about it in the [blog post here](https://dasith.me/2020/03/21/simple-endpoints/). 12 | 13 | ## Getting Started 14 | 15 | 1. Install and reference the Nuget `SimpleEndpoints` 16 | 17 | In the NuGet Package Manager Console, type: 18 | 19 | ``` 20 | Install-Package SimpleEndpoints 21 | ``` 22 | 23 | 2. Define your request and response models 24 | ```C# 25 | public class SimpleMessage 26 | { 27 | public string Message { get; set; } 28 | } 29 | 30 | public class SimpleResponse 31 | { 32 | public string Message { get; set; } 33 | } 34 | ``` 35 | 3. Create your endpoint and implement your business logic (You can choose to handle in place or dispatch to a domain layer) 36 | ```C# 37 | public class SimpleMessageEndpoint : AsyncGetEndpoint 38 | { 39 | public override async Task> HandleAsync(SimpleMessage requestModel, CancellationToken cancellationToken = default) 40 | { 41 | // Handle in place or dispatch to the domain i.e. return await _someDomainService.HandleAsync(requestModel) 42 | 43 | return new SimpleResponse() 44 | { 45 | Message = "Hello " + requestModel.Message 46 | }; 47 | } 48 | } 49 | ``` 50 | 4. In the `ConfigureServices()` method in your `Startup.cs` add the following 51 | ```C# 52 | public void ConfigureServices(IServiceCollection services) 53 | { 54 | // Other services go here 55 | 56 | services.AddControllers(); 57 | services.AddSimpleEndpointsRouting(); // This is required to translate endpoint names 58 | } 59 | ``` 60 | 61 | 5. Navigate to the URL `https://localhost:port_number/simplemessage?message=world` and see the result. 62 | 63 | --- 64 | 65 | ## Want more ? 66 | 67 | Checkout the [Examples folder](https://github.com/dasiths/SimpleEndpoints/tree/master/src/SimpleEndpoints.Example) for more. Await more examples in the coming weeks. 68 | 69 | The Endpoints are automatically inherited from a [`ControllerBase`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controllerbase?view=aspnetcore-3.1) and decorated with [`ApiController` attribute](https://www.strathweb.com/2018/02/exploring-the-apicontrollerattribute-and-its-features-for-asp-net-core-mvc-2-1/). You can decorate the endpoint class/action method with the usual (Route, HttpGet, FromQuery etc) attributes to customise and extend the functionality. Endpoints fully support [AspNetCore routing conventions](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.1). 70 | 71 | If you really need an endpoint with a custom route, a mix of parameters coming from the Route/Query/Body or need full control over of any aspect then you can do something like this. Each of these class/method attributes works independently of each other and you can pick and choose them as required. 72 | 73 | ```c# 74 | [SimpleEndpoint(HttpVerb.Get)] // define the HTTP method being used 75 | [Route("custom/[endpoint]")] // use your own route template (i.e this results in "custom/mysimple") 76 | public class MySimpleEndpoint : SimpleEndpointBase // Extend the SimpleEndpointBase class 77 | { 78 | // definew your custom signature binding attributes 79 | [Route("custom-method")] 80 | public ActionResult MyMethodName([FromQuery] int id, [FromBody] GreetingRequest requestModel) // Mix of binding attributes 81 | { 82 | return new GreetingResponse() 83 | { 84 | Greeting = $"Hello {requestModel.name} with id {id}" 85 | }; 86 | } 87 | } 88 | ``` 89 | 90 | I've had good success with creating a folder structure like below. 91 | 92 | ![Folder Structure](/assets/simple-ednpoints-folderstructure.png) 93 | 94 | You can take this one step further and create a **folder per feature group**, then put each endpoint specific folder inside that if you want as well. I recommend keeping the view models in the same folder as it's easier to find related code when they sit next to each other. 95 | 96 | --- 97 | 98 | Feel free to contribute and raise issues as you see fit :) 99 | 100 | - Creator: Dasith Wijesiriwardena (https://dasith.me) 101 | -------------------------------------------------------------------------------- /src/SimpleEndpoints.Tests/Routing/HttpMethodMetadataEnricherShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 8 | using Microsoft.AspNetCore.Routing; 9 | using Microsoft.Extensions.Logging.Abstractions; 10 | using Microsoft.Extensions.Options; 11 | using Shouldly; 12 | using SimpleEndpoints.Enrichers; 13 | using SimpleEndpoints.VerbScoped; 14 | using Xunit; 15 | 16 | namespace SimpleEndpoints.Tests.Routing 17 | { 18 | public class HttpMethodMetadataEnricherShould 19 | { 20 | [Fact] 21 | public void MapHttpDeleteFromAsyncGetEndpointBaseClass() 22 | { 23 | //Arrange 24 | var enricher = new HttpMethodEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 25 | NullLogger.Instance); 26 | var controller = CreateController(typeof(TestDeleteEndpoint)); 27 | 28 | //Act 29 | enricher.Enrich(controller, c => { }); 30 | 31 | //Assert 32 | controller.Selectors[0].EndpointMetadata.Count.ShouldBe(1); 33 | controller.Selectors[0].EndpointMetadata.First().ShouldBeOfType(); 34 | controller.Selectors[0].EndpointMetadata.OfType().First().HttpMethods 35 | .ShouldBe(new[] {"DELETE"}); 36 | } 37 | 38 | [Fact] 39 | public void MapHttpGetFromAsyncGetEndpointBaseClass() 40 | { 41 | //Arrange 42 | var enricher = new HttpMethodEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 43 | NullLogger.Instance); 44 | var controller = CreateController(typeof(TestGetEndpoint)); 45 | 46 | //Act 47 | enricher.Enrich(controller, c => { }); 48 | 49 | //Assert 50 | controller.Selectors[0].EndpointMetadata.Count.ShouldBe(1); 51 | controller.Selectors[0].EndpointMetadata.First().ShouldBeOfType(); 52 | controller.Selectors[0].EndpointMetadata.OfType().First().HttpMethods 53 | .ShouldBe(new[] {"GET"}); 54 | } 55 | 56 | [Fact] 57 | public void MapHttpPostFromAsyncGetEndpointBaseClass() 58 | { 59 | //Arrange 60 | var enricher = new HttpMethodEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 61 | NullLogger.Instance); 62 | var controller = CreateController(typeof(TestPostEndpoint)); 63 | 64 | //Act 65 | enricher.Enrich(controller, c => { }); 66 | 67 | //Assert 68 | controller.Selectors[0].EndpointMetadata.Count.ShouldBe(1); 69 | controller.Selectors[0].EndpointMetadata.First().ShouldBeOfType(); 70 | controller.Selectors[0].EndpointMetadata.OfType().First().HttpMethods 71 | .ShouldBe(new[] {"POST"}); 72 | } 73 | 74 | [Fact] 75 | public void MapHttpPutFromAsyncGetEndpointBaseClass() 76 | { 77 | //Arrange 78 | var enricher = new HttpMethodEndpointMetadataEnricher(Options.Create(new SimpleEndpointsConfiguration()), 79 | NullLogger.Instance); 80 | var controller = CreateController(typeof(TestPutEndpoint)); 81 | 82 | //Act 83 | enricher.Enrich(controller, c => { }); 84 | 85 | //Assert 86 | controller.Selectors[0].EndpointMetadata.Count.ShouldBe(1); 87 | controller.Selectors[0].EndpointMetadata.First().ShouldBeOfType(); 88 | controller.Selectors[0].EndpointMetadata.OfType().First().HttpMethods 89 | .ShouldBe(new[] {"PUT"}); 90 | } 91 | 92 | private static ControllerModel CreateController(Type typeEndpoint) 93 | { 94 | var classAttributes = Attribute.GetCustomAttributes(typeEndpoint); 95 | return new ControllerModel(typeEndpoint.GetTypeInfo(), classAttributes) 96 | { 97 | Selectors = {new SelectorModel {EndpointMetadata = { }}} 98 | }; 99 | } 100 | 101 | public class TestDeleteEndpoint : AsyncDeleteEndpoint 102 | { 103 | public override async Task 104 | HandleAsync(int model, CancellationToken cancellationToken = default) => 105 | await Task.FromResult(Ok(new {Success = true})); 106 | } 107 | 108 | public class TestGetEndpoint : AsyncGetEndpoint 109 | { 110 | public override async Task HandleAsync(CancellationToken cancellationToken = default) => 111 | await Task.FromResult(Ok(new {Success = true})); 112 | } 113 | 114 | public class TestPostEndpoint : AsyncPostEndpoint 115 | { 116 | public override async Task 117 | HandleAsync(int model, CancellationToken cancellationToken = default) => 118 | await Task.FromResult(Ok(new {Success = true})); 119 | } 120 | 121 | public class TestPutEndpoint : AsyncPutEndpoint 122 | { 123 | public override async Task 124 | HandleAsync(int model, CancellationToken cancellationToken = default) => 125 | await Task.FromResult(Ok(new {Success = true})); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb --------------------------------------------------------------------------------