diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index cf610fef4c4b3f85ed88d14d8f81adb3ef903734..e7e6269d0205a0d1ac0b8fcf8583afa59ca2a117 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -99,6 +99,8 @@ namespace Microsoft.AspNetCore.Builder Logging = new LoggingBuilder(Services); Host = new ConfigureHostBuilder(hostContext, Configuration, Services); WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services); + + Services.AddSingleton<IConfiguration>(Configuration); } /// <summary> @@ -171,6 +173,17 @@ namespace Microsoft.AspNetCore.Builder // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder. foreach (var s in _services) { + // Skip the configuration manager instance we added earlier + // we're already going to wire it up to this new configuration source + // after we've built the application. There's a chance the user manually added + // this as well but we still need to remove it from the final configuration + // to avoid cycles in the configuration graph + if (s.ServiceType == typeof(IConfiguration) && + s.ImplementationInstance == Configuration) + { + continue; + } + services.Add(s); } diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index 672e9be735882ef1fa0d9c35e0040e838026e953..e475c00062ce81e4fc47e706eb9a8b8ad4e00df7 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -693,6 +693,39 @@ namespace Microsoft.AspNetCore.Tests Assert.Contains("NewHost", options.AllowedHosts); } + [Fact] + public void CanResolveIConfigurationBeforeBuildingApplication() + { + var builder = WebApplication.CreateBuilder(); + var sp = builder.Services.BuildServiceProvider(); + + var config = sp.GetService<IConfiguration>(); + Assert.NotNull(config); + Assert.Same(config, builder.Configuration); + + var app = builder.Build(); + + // These are different + Assert.NotSame(app.Configuration, builder.Configuration); + } + + [Fact] + public void ManuallyAddingConfigurationAsServiceWorks() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddSingleton<IConfiguration>(builder.Configuration); + var sp = builder.Services.BuildServiceProvider(); + + var config = sp.GetService<IConfiguration>(); + Assert.NotNull(config); + Assert.Same(config, builder.Configuration); + + var app = builder.Build(); + + // These are different + Assert.NotSame(app.Configuration, builder.Configuration); + } + [Fact] public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig() { diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 0560be6e33373d6cf2f6eab64b2c040e22f646ed..972d741044d4e6ed4d6628e932f2918b8fd7fc14 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -476,7 +476,7 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Action<HttpContext, float>)Store, "0.5", 0.5f }, new object[] { (Action<HttpContext, Half>)Store, "0.5", (Half)0.5f }, new object[] { (Action<HttpContext, decimal>)Store, "0.5", 0.5m }, - new object[] { (Action<HttpContext, DateTime>)Store, now.ToString("o"), now }, + new object[] { (Action<HttpContext, DateTime>)Store, now.ToString("o"), now.ToUniversalTime() }, new object[] { (Action<HttpContext, DateTimeOffset>)Store, "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch }, new object[] { (Action<HttpContext, TimeSpan>)Store, "00:00:42", TimeSpan.FromSeconds(42) }, new object[] { (Action<HttpContext, Guid>)Store, "00000000-0000-0000-0000-000000000000", Guid.Empty }, @@ -2889,6 +2889,166 @@ namespace Microsoft.AspNetCore.Routing.Internal } } + public static IEnumerable<object?[]> DateTimeDelegates + { + get + { + string dateTimeParsing(DateTime time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Kind: {time.Kind}"; + + return new List<object?[]> + { + new object?[] { (Func<DateTime, string>)dateTimeParsing, "9/20/2021 4:18:44 PM", "Time: 2021-09-20T16:18:44.0000000, Kind: Unspecified" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20 4:18:44", "Time: 2021-09-20T04:18:44.0000000, Kind: Unspecified" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, " 9/20/2021 4:18:44 PM ", "Time: 2021-09-20T16:18:44.0000000, Kind: Unspecified" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20T16:28:02.000-07:00", "Time: 2021-09-20T23:28:02.0000000Z, Kind: Utc" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, " 2021-09-20T 16:28:02.000-07:00 ", "Time: 2021-09-20T23:28:02.0000000Z, Kind: Utc" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20T23:30:02.000+00:00", "Time: 2021-09-20T23:30:02.0000000Z, Kind: Utc" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, " 2021-09-20T23:30: 02.000+00:00 ", "Time: 2021-09-20T23:30:02.0000000Z, Kind: Utc" }, + new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20 16:48:02-07:00", "Time: 2021-09-20T23:48:02.0000000Z, Kind: Utc" }, + }; + } + } + + [Theory] + [MemberData(nameof(DateTimeDelegates))] + public async Task RequestDelegateCanProcessDateTimesToUtc(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> + { + ["time"] = inputTime + }); + + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + + public static IEnumerable<object?[]> DateTimeOffsetDelegates + { + get + { + string dateTimeOffsetParsing(DateTimeOffset time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Offset: {time.Offset}"; + + return new List<object?[]> + { + new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 16:35:12 +00:00", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, + new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 11:35:12 +07:00", "Time: 2021-09-20T11:35:12.0000000+07:00, Offset: 07:00:00" }, + new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 16:35:12", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, + new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, " 09/20/2021 16:35:12 ", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, + }; + } + } + + [Theory] + [MemberData(nameof(DateTimeOffsetDelegates))] + public async Task RequestDelegateCanProcessDateTimeOffsetsToUtc(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> + { + ["time"] = inputTime + }); + + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + + public static IEnumerable<object?[]> DateOnlyDelegates + { + get + { + string dateOnlyParsing(DateOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; + + return new List<object?[]> + { + new object?[] { (Func<DateOnly, string>)dateOnlyParsing, "9/20/2021", "Time: 2021-09-20" }, + new object?[] { (Func<DateOnly, string>)dateOnlyParsing, "9 /20 /2021", "Time: 2021-09-20" }, + }; + } + } + + [Theory] + [MemberData(nameof(DateOnlyDelegates))] + public async Task RequestDelegateCanProcessDateOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> + { + ["time"] = inputTime + }); + + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + + public static IEnumerable<object?[]> TimeOnlyDelegates + { + get + { + string timeOnlyParsing(TimeOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; + + return new List<object?[]> + { + new object?[] { (Func<TimeOnly, string>)timeOnlyParsing, "4:34 PM", "Time: 16:34:00.0000000" }, + new object?[] { (Func<TimeOnly, string>)timeOnlyParsing, " 4:34 PM ", "Time: 16:34:00.0000000" }, + }; + } + } + + [Theory] + [MemberData(nameof(TimeOnlyDelegates))] + public async Task RequestDelegateCanProcessTimeOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; + + httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues> + { + ["time"] = inputTime + }); + + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + private DefaultHttpContext CreateHttpContext() { var responseFeature = new TestHttpResponseFeature(); diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs index d4b73c4065f9fc2e504782f452997a514f38ce26..1b80242b80159565adca60c4d42bbe0d438bff7c 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs @@ -45,7 +45,10 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore /// <inheritdoc /> public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next) { - if (!(errorContext.Exception is DbException)) + var dbException = errorContext.Exception as DbException + ?? errorContext.Exception?.InnerException as DbException; + + if (dbException == null) { await next(errorContext); return; @@ -76,7 +79,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore { var page = new DatabaseErrorPage { - Model = new DatabaseErrorPageModel(errorContext.Exception, contextDetails, _options, errorContext.HttpContext.Request.PathBase) + Model = new DatabaseErrorPageModel(dbException, contextDetails, _options, errorContext.HttpContext.Request.PathBase) }; await page.ExecuteAsync(errorContext.HttpContext); diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseDeveloperPageExceptionFilterTests.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseDeveloperPageExceptionFilterTests.cs index 4889b1bba98131e8725c9aa09de52bea47dcedd1..8f0b3e885ea8966023adc191bd0111a6c2f1d6d9 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseDeveloperPageExceptionFilterTests.cs +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseDeveloperPageExceptionFilterTests.cs @@ -42,6 +42,33 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests Assert.True(nextFilterInvoked); } + [Fact] + public async Task Wrapped_DbExceptions_HandlingFails_InvokesNextFilter() + { + var sink = new TestSink(); + var filter = new DatabaseDeveloperPageExceptionFilter( + new TestLogger<DatabaseDeveloperPageExceptionFilter>(new TestLoggerFactory(sink, true)), + Options.Create(new DatabaseErrorPageOptions())); + var context = new DefaultHttpContext(); + var exception = new InvalidOperationException("Bang!", new Mock<DbException>().Object); + var nextFilterInvoked = false; + + await filter.HandleExceptionAsync( + new ErrorContext(context, exception), + context => + { + nextFilterInvoked = true; + return Task.CompletedTask; + }); + + Assert.True(nextFilterInvoked); + Assert.Equal(1, sink.Writes.Count); + var message = sink.Writes.Single(); + Assert.Equal(LogLevel.Error, message.LogLevel); + Assert.Contains("An exception occurred while calculating the database error page content.", message.Message); + } + + [Fact] public async Task DbExceptions_HandlingFails_InvokesNextFilter() { diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs index a5523a08c176121ccfaa3d5981538b47f21e282d..2329d42dafe348b93958bf91dce93ae50ac28581 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs @@ -41,6 +41,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthorization(); #if (EnableOpenAPI) +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); #endif diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs index bc5064e57ac00b9df8a9675234844a6b49950c7b..f8d584ca193ee57b926e7707ca92a97ec21555f8 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.MinimalAPIs.WindowsOrNoAuth.cs @@ -2,6 +2,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. #if (EnableOpenAPI) +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); #endif diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs index 5c53385a9fcb06b01a3306a8310bf8bc2733ddcf..0f514a3f65c603d645f2df35ae6881d72519540a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Program.cs @@ -43,11 +43,9 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddControllers(); #if (EnableOpenAPI) +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => -{ - c.SwaggerDoc("v1", new() { Title = "Company.WebApplication1", Version = "v1" }); -}); +builder.Services.AddSwaggerGen(); #endif var app = builder.Build(); @@ -57,7 +55,7 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Company.WebApplication1 v1")); + app.UseSwaggerUI(); } #endif #if (RequiresHttps) diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index be7f48c77f2f5530fa6245b2349b0646e28ac552..e074ef0c159e4fb14d595ba67bce5c1e412585e0 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -88,11 +88,30 @@ namespace Microsoft.AspNetCore.Http if (TryGetDateTimeTryParseMethod(type, out methodInfo)) { + // We generate `DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces ` to + // support parsing types into the UTC timezone for DateTime. We don't assume the timezone + // on the original value which will cause the parser to set the `Kind` property on the + // `DateTime` as `Unspecified` indicating that it was parsed from an ambiguous timezone. + // + // `DateTimeOffset`s are always in UTC and don't allow specifying an `Unspecific` kind. + // For this, we always assume that the original value is already in UTC to avoid resolving + // the offset incorrectly dependening on the timezone of the machine. We don't bother mapping + // it to UTC in this case. In the event that the original timestamp is not in UTC, it's offset + // value will be maintained. + // + // DateOnly and TimeOnly types do not support conversion to Utc so we + // default to `DateTimeStyles.AllowWhiteSpaces`. + var dateTimeStyles = type switch { + Type t when t == typeof(DateTime) => DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces, + Type t when t == typeof(DateTimeOffset) => DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces, + _ => DateTimeStyles.AllowWhiteSpaces + }; + return (expression) => Expression.Call( methodInfo!, TempSourceStringExpr, Expression.Constant(CultureInfo.InvariantCulture), - Expression.Constant(DateTimeStyles.None), + Expression.Constant(dateTimeStyles), expression); }