diff --git a/README.md b/README.md
index 0845b31..8adf979 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,58 @@
-# vegasco-server
\ No newline at end of file
+# 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"
+ }
+}
+```
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
deleted file mode 100644
index 346136f..0000000
--- a/src/Api/Api.csproj
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 4bf893d3-0c16-41ec-8b46-2768d841215d
- Linux
- ..\..
- Vegasco.Api
-
-
-
-
-
-
-
-
-
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
deleted file mode 100644
index b072d92..0000000
--- a/src/Api/Program.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using Vegasco.Api;
-
-WebApplication.CreateBuilder(args)
- .ConfigureServices()
- .ConfigureRequestPipeline()
- .Run();
diff --git a/src/Api/StartupExtensions.cs b/src/Api/StartupExtensions.cs
deleted file mode 100644
index a785df1..0000000
--- a/src/Api/StartupExtensions.cs
+++ /dev/null
@@ -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;
- }
-}
\ No newline at end of file
diff --git a/src/WebApi/Authentication/JwtOptions.cs b/src/WebApi/Authentication/JwtOptions.cs
new file mode 100644
index 0000000..e05daa0
--- /dev/null
+++ b/src/WebApi/Authentication/JwtOptions.cs
@@ -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
+{
+ public JwtOptionsValidator()
+ {
+ RuleFor(x => x.Audience)
+ .NotEmpty();
+
+ RuleFor(x => x.Authority)
+ .NotEmpty();
+
+ RuleFor(x => x.Issuer)
+ .NotEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/WebApi/Cars/Car.cs b/src/WebApi/Cars/Car.cs
new file mode 100644
index 0000000..da1e72b
--- /dev/null
+++ b/src/WebApi/Cars/Car.cs
@@ -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; }
+}
diff --git a/src/WebApi/Cars/CreateCar.cs b/src/WebApi/Cars/CreateCar.cs
new file mode 100644
index 0000000..ed7630e
--- /dev/null
+++ b/src/WebApi/Cars/CreateCar.cs
@@ -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
+ {
+ public Validator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty();
+ }
+ }
+
+ public static async Task Handler(Request request, IEnumerable> validators)
+ {
+ List failedValidations = await validators.ValidateAllAsync(request);
+ if (failedValidations.Count > 0)
+ {
+ return Results.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary()));
+ }
+
+ return Results.Ok();
+ }
+}
diff --git a/src/WebApi/Common/Constants.cs b/src/WebApi/Common/Constants.cs
new file mode 100644
index 0000000..bc15221
--- /dev/null
+++ b/src/WebApi/Common/Constants.cs
@@ -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";
+ }
+}
\ No newline at end of file
diff --git a/src/WebApi/Common/DependencyInjectionExtensions.cs b/src/WebApi/Common/DependencyInjectionExtensions.cs
new file mode 100644
index 0000000..19de60f
--- /dev/null
+++ b/src/WebApi/Common/DependencyInjectionExtensions.cs
@@ -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
+{
+ ///
+ /// Adds all the WebApi related services to the Dependency Injection container.
+ ///
+ ///
+ 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();
+
+ return services;
+ }
+
+ private static IServiceCollection AddOpenApi(this IServiceCollection services)
+ {
+ services.ConfigureOptions();
+
+ 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()
+ .BindConfiguration(JwtOptions.SectionName)
+ .ValidateFluently()
+ .ValidateOnStart();
+
+ var jwtOptions = services.BuildServiceProvider().GetRequiredService>();
+
+ 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;
+ }
+}
diff --git a/src/WebApi/Common/IWebApiMarker.cs b/src/WebApi/Common/IWebApiMarker.cs
new file mode 100644
index 0000000..bcd335d
--- /dev/null
+++ b/src/WebApi/Common/IWebApiMarker.cs
@@ -0,0 +1,3 @@
+namespace Vegasco.WebApi.Common;
+
+public interface IWebApiMarker;
diff --git a/src/WebApi/Common/StartupExtensions.cs b/src/WebApi/Common/StartupExtensions.cs
new file mode 100644
index 0000000..2e1b332
--- /dev/null
+++ b/src/WebApi/Common/StartupExtensions.cs
@@ -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 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;
+ }
+}
diff --git a/src/WebApi/Common/ValidatorExtensions.cs b/src/WebApi/Common/ValidatorExtensions.cs
new file mode 100644
index 0000000..2ee107f
--- /dev/null
+++ b/src/WebApi/Common/ValidatorExtensions.cs
@@ -0,0 +1,94 @@
+using FluentValidation;
+using FluentValidation.Results;
+using Microsoft.Extensions.Options;
+
+namespace Vegasco.WebApi.Common;
+
+public static class ValidatorExtensions
+{
+ ///
+ /// Asynchronously validates an instance of against all instances in .
+ ///
+ ///
+ ///
+ ///
+ /// The failed validation results.
+ public static async Task> ValidateAllAsync(this IEnumerable> validators, T instance)
+ {
+ var validationTasks = validators
+ .Select(validator => validator.ValidateAsync(instance))
+ .ToList();
+
+ await Task.WhenAll(validationTasks);
+
+ List failedValidations = validationTasks
+ .Select(x => x.Result)
+ .Where(x => !x.IsValid)
+ .ToList();
+
+ return failedValidations;
+ }
+
+ public static Dictionary ToCombinedDictionary(this IEnumerable validationResults)
+ {
+ // Use a hash set to avoid duplicate error messages.
+ Dictionary> combinedErrors = [];
+
+ foreach (var error in validationResults.SelectMany(x => x.Errors))
+ {
+ if (!combinedErrors.TryGetValue(error.PropertyName, out HashSet? 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 ValidateFluently(this OptionsBuilder builder)
+ where T : class
+ {
+ builder.Services.AddTransient>(serviceProvider =>
+ {
+ var validators = serviceProvider.GetServices>() ?? [];
+ return new FluentValidationOptions(builder.Name, validators);
+ });
+ return builder;
+ }
+}
+
+internal class FluentValidationOptions : IValidateOptions
+ where TOptions : class
+{
+ private readonly IEnumerable> _validators;
+
+ public string? Name { get; set; }
+
+ public FluentValidationOptions(string? name, IEnumerable> 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)));
+ }
+}
\ No newline at end of file
diff --git a/src/Api/Dockerfile b/src/WebApi/Dockerfile
similarity index 100%
rename from src/Api/Dockerfile
rename to src/WebApi/Dockerfile
diff --git a/src/WebApi/Endpoints/EndpointExtensions.cs b/src/WebApi/Endpoints/EndpointExtensions.cs
new file mode 100644
index 0000000..17cc430
--- /dev/null
+++ b/src/WebApi/Endpoints/EndpointExtensions.cs
@@ -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(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);
+ }
+}
diff --git a/src/WebApi/Endpoints/IEndpoint.cs b/src/WebApi/Endpoints/IEndpoint.cs
new file mode 100644
index 0000000..15e0c50
--- /dev/null
+++ b/src/WebApi/Endpoints/IEndpoint.cs
@@ -0,0 +1,6 @@
+namespace Vegasco.WebApi.Endpoints;
+
+public interface IEndpoint
+{
+ void MapEndpoint(IEndpointRouteBuilder builder);
+}
diff --git a/src/WebApi/Endpoints/OpenApi/ConfigureSwaggerGenOptions.cs b/src/WebApi/Endpoints/OpenApi/ConfigureSwaggerGenOptions.cs
new file mode 100644
index 0000000..571dde8
--- /dev/null
+++ b/src/WebApi/Endpoints/OpenApi/ConfigureSwaggerGenOptions.cs
@@ -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;
+
+///
+/// Registers each api version as its own swagger document.
+///
+///
+public class ConfigureSwaggerGenOptions(
+ IApiVersionDescriptionProvider versionDescriptionProvider)
+ : IConfigureNamedOptions
+{
+ 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() }
+ });
+
+ OpenApiInfo openApiInfo = new()
+ {
+ Title = "Vegasco API",
+ Version = description.ApiVersion.ToString()
+ };
+
+ options.SwaggerDoc(description.GroupName, openApiInfo);
+ }
+ }
+
+ public void Configure(string? name, SwaggerGenOptions options)
+ {
+ Configure(options);
+ }
+}
diff --git a/src/WebApi/Endpoints/OpenApi/SwaggerDocConstants.cs b/src/WebApi/Endpoints/OpenApi/SwaggerDocConstants.cs
new file mode 100644
index 0000000..e88de76
--- /dev/null
+++ b/src/WebApi/Endpoints/OpenApi/SwaggerDocConstants.cs
@@ -0,0 +1,6 @@
+namespace Vegasco.WebApi.Endpoints.OpenApi;
+
+public static class SwaggerDocConstants
+{
+
+}
diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs
new file mode 100644
index 0000000..fd643eb
--- /dev/null
+++ b/src/WebApi/Program.cs
@@ -0,0 +1,6 @@
+using Vegasco.WebApi.Common;
+
+WebApplication.CreateBuilder(args)
+ .ConfigureServices()
+ .ConfigureRequestPipeline()
+ .Run();
diff --git a/src/Api/Properties/launchSettings.json b/src/WebApi/Properties/launchSettings.json
similarity index 100%
rename from src/Api/Properties/launchSettings.json
rename to src/WebApi/Properties/launchSettings.json
diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj
new file mode 100644
index 0000000..aaa80ac
--- /dev/null
+++ b/src/WebApi/WebApi.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+ 4bf893d3-0c16-41ec-8b46-2768d841215d
+ Linux
+ ..\..
+ Vegasco.WebApi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Api/appsettings.Development.json b/src/WebApi/appsettings.Development.json
similarity index 100%
rename from src/Api/appsettings.Development.json
rename to src/WebApi/appsettings.Development.json
diff --git a/src/Api/appsettings.json b/src/WebApi/appsettings.json
similarity index 100%
rename from src/Api/appsettings.json
rename to src/WebApi/appsettings.json
diff --git a/vegasco-server.sln b/vegasco-server.sln
index 366336c..da38768 100644
--- a/vegasco-server.sln
+++ b/vegasco-server.sln
@@ -1,9 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
+# 17
VisualStudioVersion = 17.0.31903.59
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -19,4 +26,10 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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