New Angular based web version #1
59
README.md
59
README.md
@@ -1 +1,58 @@
|
|||||||
# vegasco-server
|
# Vegasco Server
|
||||||
|
|
||||||
|
Backend for the vegasco (***VE***hicle ***GAS*** ***CO***nsumption) application.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Configuration | Description | Default | Required |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| JWT:Authority | The authority of the JWT token. | - | true |
|
||||||
|
| JWT:Audience | The audience of the JWT token. | - | true |
|
||||||
|
| JWT:Issuer | The issuer of the JWT token. | - | true |
|
||||||
|
| JWT:NameClaimType | The type of the name claim. | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` (C# constant `ClaimTypes.Name` | false |
|
||||||
|
|
||||||
|
The application uses the prefix `Vegasco_` for environment variable names. The prefix is removed when the application reads the environment variables and duplicate entries are overwritten by the environment variables.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
- `foo=bar1`
|
||||||
|
- `Vegasco_foo=bar2`
|
||||||
|
|
||||||
|
Results in:
|
||||||
|
|
||||||
|
- `foo=bar2`
|
||||||
|
- `Vegasco_foo=bar2`
|
||||||
|
|
||||||
|
Configuration hierarchy in environment variables is usually denoted using a colon (`:`). But because on some systems the colon character is a reserved character, you can use a double underscore (`__`) as an alternative. The application will replace the double underscore with a colon when reading the environment variables.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
The environment variable `foo__bar=value` (as well as `Vegasco_foo__bar=value`) will be converted to `foo:bar=value` in the application.
|
||||||
|
|
||||||
|
### Configuration examples
|
||||||
|
|
||||||
|
As environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
Vegasco_JWT__Authority=https://example.authority.com
|
||||||
|
Vegasco_JWT__Audience=example-audience
|
||||||
|
Vegasco_JWT__Issuer=https://example.authority.com/realms/example-realm/
|
||||||
|
Vegasco_JWT__NameClaimType=preferred_username
|
||||||
|
```
|
||||||
|
|
||||||
|
As appsettings.json (or a environment specific appsettings.*.json):
|
||||||
|
|
||||||
|
**Note: the `Vegasco_` prefix is only for environment variables**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"JWT": {
|
||||||
|
"Authority": "https://example.authority.com/realms/example-realm",
|
||||||
|
"Audience": "example-audience",
|
||||||
|
"Issuer": "https://example.authority.com/realms/example-realm/",
|
||||||
|
"NameClaimType": "preferred_username"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UserSecretsId>4bf893d3-0c16-41ec-8b46-2768d841215d</UserSecretsId>
|
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
|
||||||
<DockerfileContext>..\..</DockerfileContext>
|
|
||||||
<RootNamespace>Vegasco.Api</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
using Vegasco.Api;
|
|
||||||
|
|
||||||
WebApplication.CreateBuilder(args)
|
|
||||||
.ConfigureServices()
|
|
||||||
.ConfigureRequestPipeline()
|
|
||||||
.Run();
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
namespace Vegasco.Api;
|
|
||||||
|
|
||||||
internal static class StartupExtensions
|
|
||||||
{
|
|
||||||
internal static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
|
|
||||||
builder.Services.AddHealthChecks();
|
|
||||||
|
|
||||||
return builder.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static WebApplication ConfigureRequestPipeline(this WebApplication app)
|
|
||||||
{
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.MapHealthChecks("/health");
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
src/WebApi/Authentication/JwtOptions.cs
Normal file
31
src/WebApi/Authentication/JwtOptions.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Authentication;
|
||||||
|
|
||||||
|
public class JwtOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "JWT";
|
||||||
|
|
||||||
|
public string Audience { get; set; } = "";
|
||||||
|
|
||||||
|
public string Authority { get; set; } = "";
|
||||||
|
|
||||||
|
public string Issuer { get; set; } = "";
|
||||||
|
|
||||||
|
public string? NameClaimType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JwtOptionsValidator : AbstractValidator<JwtOptions>
|
||||||
|
{
|
||||||
|
public JwtOptionsValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Audience)
|
||||||
|
.NotEmpty();
|
||||||
|
|
||||||
|
RuleFor(x => x.Authority)
|
||||||
|
.NotEmpty();
|
||||||
|
|
||||||
|
RuleFor(x => x.Issuer)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/WebApi/Cars/Car.cs
Normal file
10
src/WebApi/Cars/Car.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Vegasco.WebApi.Cars;
|
||||||
|
|
||||||
|
public class Car
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
}
|
||||||
38
src/WebApi/Cars/CreateCar.cs
Normal file
38
src/WebApi/Cars/CreateCar.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Cars;
|
||||||
|
|
||||||
|
public static class CreateCar
|
||||||
|
{
|
||||||
|
public record Request(string Name);
|
||||||
|
public record Response(Guid Id, string Name);
|
||||||
|
|
||||||
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
return builder
|
||||||
|
.MapPost("cars", Handler)
|
||||||
|
.WithTags("Cars");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Validator : AbstractValidator<Request>
|
||||||
|
{
|
||||||
|
public Validator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Name)
|
||||||
|
.NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IResult> Handler(Request request, IEnumerable<IValidator<Request>> validators)
|
||||||
|
{
|
||||||
|
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request);
|
||||||
|
if (failedValidations.Count > 0)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/WebApi/Common/Constants.cs
Normal file
11
src/WebApi/Common/Constants.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string AppOtelName = "Vegasco.Api";
|
||||||
|
|
||||||
|
public static class Authorization
|
||||||
|
{
|
||||||
|
public const string RequireAuthenticatedUserPolicy = "RequireAuthenticatedUser";
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/WebApi/Common/DependencyInjectionExtensions.cs
Normal file
124
src/WebApi/Common/DependencyInjectionExtensions.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using Asp.Versioning;
|
||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Vegasco.WebApi.Authentication;
|
||||||
|
using Vegasco.WebApi.Endpoints;
|
||||||
|
using Vegasco.WebApi.Endpoints.OpenApi;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds all the WebApi related services to the Dependency Injection container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"></param>
|
||||||
|
public static void AddWebApiServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddMiscellaneousServices()
|
||||||
|
.AddOpenApi()
|
||||||
|
.AddApiVersioning()
|
||||||
|
.AddOtel()
|
||||||
|
.AddAuthenticationAndAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddResponseCompression();
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssemblies(
|
||||||
|
[
|
||||||
|
typeof(IWebApiMarker).Assembly
|
||||||
|
], ServiceLifetime.Singleton);
|
||||||
|
|
||||||
|
services.AddHealthChecks();
|
||||||
|
services.AddEndpointsFromAssemblyContaining<IWebApiMarker>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddOpenApi(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.ConfigureOptions<ConfigureSwaggerGenOptions>();
|
||||||
|
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
services.AddSwaggerGen();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddApiVersioning(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddApiVersioning(o =>
|
||||||
|
{
|
||||||
|
o.DefaultApiVersion = new ApiVersion(1);
|
||||||
|
o.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||||
|
o.ReportApiVersions = true;
|
||||||
|
})
|
||||||
|
.AddApiExplorer(o =>
|
||||||
|
{
|
||||||
|
o.GroupNameFormat = "'v'V";
|
||||||
|
o.SubstituteApiVersionInUrl = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddOtel(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
|
||||||
|
|
||||||
|
ActivitySource activitySource = new(Constants.AppOtelName);
|
||||||
|
services.AddSingleton(activitySource);
|
||||||
|
|
||||||
|
services.AddOpenTelemetry()
|
||||||
|
.WithTracing(t =>
|
||||||
|
{
|
||||||
|
t.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddOtlpExporter()
|
||||||
|
.AddSource(activitySource.Name);
|
||||||
|
})
|
||||||
|
.WithMetrics();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddAuthenticationAndAuthorization(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddOptions<JwtOptions>()
|
||||||
|
.BindConfiguration(JwtOptions.SectionName)
|
||||||
|
.ValidateFluently()
|
||||||
|
.ValidateOnStart();
|
||||||
|
|
||||||
|
var jwtOptions = services.BuildServiceProvider().GetRequiredService<IOptions<JwtOptions>>();
|
||||||
|
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
|
||||||
|
{
|
||||||
|
o.Authority = jwtOptions.Value.Authority;
|
||||||
|
|
||||||
|
o.TokenValidationParameters.ValidAudience = jwtOptions.Value.Audience;
|
||||||
|
o.TokenValidationParameters.ValidateAudience = true;
|
||||||
|
|
||||||
|
o.TokenValidationParameters.ValidIssuer = jwtOptions.Value.Issuer;
|
||||||
|
o.TokenValidationParameters.ValidateIssuer = true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(jwtOptions.Value.NameClaimType))
|
||||||
|
{
|
||||||
|
o.TokenValidationParameters.NameClaimType = jwtOptions.Value.NameClaimType;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddAuthorizationBuilder()
|
||||||
|
.AddPolicy(Constants.Authorization.RequireAuthenticatedUserPolicy, p => p
|
||||||
|
.RequireAuthenticatedUser()
|
||||||
|
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/WebApi/Common/IWebApiMarker.cs
Normal file
3
src/WebApi/Common/IWebApiMarker.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
public interface IWebApiMarker;
|
||||||
62
src/WebApi/Common/StartupExtensions.cs
Normal file
62
src/WebApi/Common/StartupExtensions.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using Asp.Versioning.ApiExplorer;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using System.Globalization;
|
||||||
|
using Vegasco.WebApi.Endpoints;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
internal static class StartupExtensions
|
||||||
|
{
|
||||||
|
internal static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
||||||
|
|
||||||
|
builder.Services.AddWebApiServices();
|
||||||
|
|
||||||
|
WebApplication app = builder.Build();
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static WebApplication ConfigureRequestPipeline(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.UseRequestLocalization(o =>
|
||||||
|
{
|
||||||
|
o.SupportedCultures =
|
||||||
|
[
|
||||||
|
new CultureInfo("en")
|
||||||
|
];
|
||||||
|
|
||||||
|
o.SupportedUICultures = o.SupportedCultures;
|
||||||
|
|
||||||
|
CultureInfo defaultCulture = o.SupportedCultures[0];
|
||||||
|
o.DefaultRequestCulture = new RequestCulture(defaultCulture);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapEndpoints();
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(o =>
|
||||||
|
{
|
||||||
|
// Create a Swagger endpoint for each API version
|
||||||
|
IReadOnlyList<ApiVersionDescription> apiVersions = app.DescribeApiVersions();
|
||||||
|
foreach (ApiVersionDescription apiVersionDescription in apiVersions)
|
||||||
|
{
|
||||||
|
string url = $"/swagger/{apiVersionDescription.GroupName}/swagger.json";
|
||||||
|
string name = apiVersionDescription.GroupName.ToUpperInvariant();
|
||||||
|
o.SwaggerEndpoint(url, name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/WebApi/Common/ValidatorExtensions.cs
Normal file
94
src/WebApi/Common/ValidatorExtensions.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
public static class ValidatorExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously validates an instance of <typeparamref name="T"/> against all <see cref="IValidator{T}"/> instances in <paramref name="validators"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="validators"></param>
|
||||||
|
/// <param name="instance"></param>
|
||||||
|
/// <returns>The failed validation results.</returns>
|
||||||
|
public static async Task<List<ValidationResult>> ValidateAllAsync<T>(this IEnumerable<IValidator<T>> validators, T instance)
|
||||||
|
{
|
||||||
|
var validationTasks = validators
|
||||||
|
.Select(validator => validator.ValidateAsync(instance))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await Task.WhenAll(validationTasks);
|
||||||
|
|
||||||
|
List<ValidationResult> failedValidations = validationTasks
|
||||||
|
.Select(x => x.Result)
|
||||||
|
.Where(x => !x.IsValid)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return failedValidations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string[]> ToCombinedDictionary(this IEnumerable<ValidationResult> validationResults)
|
||||||
|
{
|
||||||
|
// Use a hash set to avoid duplicate error messages.
|
||||||
|
Dictionary<string, HashSet<string>> combinedErrors = [];
|
||||||
|
|
||||||
|
foreach (var error in validationResults.SelectMany(x => x.Errors))
|
||||||
|
{
|
||||||
|
if (!combinedErrors.TryGetValue(error.PropertyName, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value = ([error.ErrorMessage]);
|
||||||
|
combinedErrors[error.PropertyName] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Add(error.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedErrors.ToDictionary(x => x.Key, x => x.Value.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OptionsBuilder<T> ValidateFluently<T>(this OptionsBuilder<T> builder)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
builder.Services.AddTransient<IValidateOptions<T>>(serviceProvider =>
|
||||||
|
{
|
||||||
|
var validators = serviceProvider.GetServices<IValidator<T>>() ?? [];
|
||||||
|
return new FluentValidationOptions<T>(builder.Name, validators);
|
||||||
|
});
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FluentValidationOptions<TOptions> : IValidateOptions<TOptions>
|
||||||
|
where TOptions : class
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IValidator<TOptions>> _validators;
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public FluentValidationOptions(string? name, IEnumerable<IValidator<TOptions>> validators)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
_validators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidateOptionsResult Validate(string? name, TOptions options)
|
||||||
|
{
|
||||||
|
if (name is not null && name != Name)
|
||||||
|
{
|
||||||
|
return ValidateOptionsResult.Skip;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
|
var failedValidations = _validators.ValidateAllAsync(options).Result;
|
||||||
|
if (failedValidations.Count == 0)
|
||||||
|
{
|
||||||
|
return ValidateOptionsResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateOptionsResult.Fail(failedValidations.SelectMany(x => x.Errors.Select(x => x.ErrorMessage)));
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/WebApi/Endpoints/EndpointExtensions.cs
Normal file
39
src/WebApi/Endpoints/EndpointExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Asp.Versioning.Builder;
|
||||||
|
using Asp.Versioning.Conventions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Vegasco.WebApi.Cars;
|
||||||
|
using Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Endpoints;
|
||||||
|
|
||||||
|
public static class EndpointExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddEndpointsFromAssemblyContaining<T>(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var assembly = typeof(T).Assembly;
|
||||||
|
|
||||||
|
ServiceDescriptor[] serviceDescriptors = assembly
|
||||||
|
.DefinedTypes
|
||||||
|
.Where(type => type is { IsAbstract: false, IsInterface: false } &&
|
||||||
|
type.IsAssignableTo(typeof(IEndpoint)))
|
||||||
|
.Select(type => ServiceDescriptor.Transient(typeof(IEndpoint), type))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
services.TryAddEnumerable(serviceDescriptors);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapEndpoints(this IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
ApiVersionSet apiVersionSet = builder.NewApiVersionSet()
|
||||||
|
.HasApiVersion(1.0)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
RouteGroupBuilder versionedApis = builder.MapGroup("/v{apiVersion:apiVersion}")
|
||||||
|
.WithApiVersionSet(apiVersionSet)
|
||||||
|
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||||
|
|
||||||
|
CreateCar.MapEndpoint(versionedApis);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/WebApi/Endpoints/IEndpoint.cs
Normal file
6
src/WebApi/Endpoints/IEndpoint.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Vegasco.WebApi.Endpoints;
|
||||||
|
|
||||||
|
public interface IEndpoint
|
||||||
|
{
|
||||||
|
void MapEndpoint(IEndpointRouteBuilder builder);
|
||||||
|
}
|
||||||
56
src/WebApi/Endpoints/OpenApi/ConfigureSwaggerGenOptions.cs
Normal file
56
src/WebApi/Endpoints/OpenApi/ConfigureSwaggerGenOptions.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Asp.Versioning.ApiExplorer;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Endpoints.OpenApi;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers each api version as its own swagger document.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="versionDescriptionProvider"></param>
|
||||||
|
public class ConfigureSwaggerGenOptions(
|
||||||
|
IApiVersionDescriptionProvider versionDescriptionProvider)
|
||||||
|
: IConfigureNamedOptions<SwaggerGenOptions>
|
||||||
|
{
|
||||||
|
private readonly IApiVersionDescriptionProvider _versionDescriptionProvider = versionDescriptionProvider;
|
||||||
|
|
||||||
|
public void Configure(SwaggerGenOptions options)
|
||||||
|
{
|
||||||
|
foreach (ApiVersionDescription description in _versionDescriptionProvider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
OpenApiSecurityScheme securityScheme = new()
|
||||||
|
{
|
||||||
|
Name = "Bearer",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Id = IdentityConstants.BearerScheme,
|
||||||
|
Type = ReferenceType.SecurityScheme
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{ securityScheme, Array.Empty<string>() }
|
||||||
|
});
|
||||||
|
|
||||||
|
OpenApiInfo openApiInfo = new()
|
||||||
|
{
|
||||||
|
Title = "Vegasco API",
|
||||||
|
Version = description.ApiVersion.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
options.SwaggerDoc(description.GroupName, openApiInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(string? name, SwaggerGenOptions options)
|
||||||
|
{
|
||||||
|
Configure(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/WebApi/Endpoints/OpenApi/SwaggerDocConstants.cs
Normal file
6
src/WebApi/Endpoints/OpenApi/SwaggerDocConstants.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Vegasco.WebApi.Endpoints.OpenApi;
|
||||||
|
|
||||||
|
public static class SwaggerDocConstants
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
6
src/WebApi/Program.cs
Normal file
6
src/WebApi/Program.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
WebApplication.CreateBuilder(args)
|
||||||
|
.ConfigureServices()
|
||||||
|
.ConfigureRequestPipeline()
|
||||||
|
.Run();
|
||||||
28
src/WebApi/WebApi.csproj
Normal file
28
src/WebApi/WebApi.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>4bf893d3-0c16-41ec-8b46-2768d841215d</UserSecretsId>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<DockerfileContext>..\..</DockerfileContext>
|
||||||
|
<RootNamespace>Vegasco.WebApi</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
|
||||||
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry" Version="1.9.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj", "{9FF3C98A-5085-4EBE-A980-DB2148B0C00A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "src\WebApi\WebApi.csproj", "{9FF3C98A-5085-4EBE-A980-DB2148B0C00A}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C051A684-BD6A-43F2-B0CC-F3C2315D99E3}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A16251C2-47DB-4017-812B-CA18B280E049}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
README.md = README.md
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -19,4 +26,10 @@ Global
|
|||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{9FF3C98A-5085-4EBE-A980-DB2148B0C00A} = {C051A684-BD6A-43F2-B0CC-F3C2315D99E3}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {7813E32D-AE19-479C-853B-063882D2D05A}
|
||||||
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
Reference in New Issue
Block a user