diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index f5d803c3b822..f535f540edbc 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -186,8 +186,7 @@ internal void InitializeDefaultServices() Services.AddSingleton(DefaultWebAssemblyJSRuntime.Instance); Services.AddSingleton(WebAssemblyNavigationManager.Instance); Services.AddSingleton(WebAssemblyNavigationInterception.Instance); - Services.AddSingleton(); - Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>))); + Services.AddSingleton(s => new WebAssemblyLoggerFactory(new List { new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance) })); } } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyConsoleLogger.cs b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyConsoleLogger.cs similarity index 100% rename from src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyConsoleLogger.cs rename to src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyConsoleLogger.cs diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyConsoleLoggerProvider.cs b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyConsoleLoggerProvider.cs new file mode 100644 index 000000000000..e009d1877405 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyConsoleLoggerProvider.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; + + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services +{ + /// + /// A provider of instances. + /// + public class WebAssemblyConsoleLoggerProvider : ILoggerProvider + { + private ConcurrentDictionary> _loggers; + private IJSInProcessRuntime _jsRuntime; + private bool _disposed; + + /// + /// Creates an instance of . + /// + /// The options to create instances with. + public WebAssemblyConsoleLoggerProvider(IJSInProcessRuntime jsRuntime) + { + _loggers = new ConcurrentDictionary>(); + _jsRuntime = jsRuntime; + } + + /// + public ILogger CreateLogger(string name) + { + return _loggers.GetOrAdd(name, loggerName => new WebAssemblyConsoleLogger(name, _jsRuntime)); + } + + /// + public void Dispose() + { + if (!_disposed) + { + _loggers = null; + _jsRuntime = null; + } + _disposed = true; + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLogger.cs b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLogger.cs new file mode 100644 index 000000000000..51b60fd3a8bf --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLogger.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services +{ + internal class Logger : ILogger + { + public WebAssemblyLoggerInformation[] Loggers { get; set; } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var loggers = Loggers; + if (loggers == null) + { + return; + } + + List exceptions = null; + for (var i = 0; i < loggers.Length; i++) + { + ref readonly var loggerInfo = ref loggers[i]; + var logger = loggerInfo.Logger; + if (!logger.IsEnabled(logLevel)) + { + continue; + } + + LoggerLog(logLevel, eventId, logger, exception, formatter, ref exceptions, state); + } + + if (exceptions != null && exceptions.Count > 0) + { + ThrowLoggingError(exceptions); + } + + static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception exception, Func formatter, ref List exceptions, in TState state) + { + try + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + } + } + + /// + public bool IsEnabled(LogLevel logLevel) + { + var loggers = Loggers; + if (loggers == null) + { + return false; + } + + List exceptions = null; + var i = 0; + for (; i < loggers.Length; i++) + { + ref readonly var loggerInfo = ref loggers[i]; + var logger = loggerInfo.Logger; + if (!logger.IsEnabled(logLevel)) + { + continue; + } + + if (LoggerIsEnabled(logLevel, loggerInfo.Logger, ref exceptions)) + { + break; + } + } + + if (exceptions != null && exceptions.Count > 0) + { + ThrowLoggingError(exceptions); + } + + return i < loggers.Length ? true : false; + + static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List exceptions) + { + try + { + if (logger.IsEnabled(logLevel)) + { + return true; + } + } + catch (Exception ex) + { + if (exceptions == null) + { + exceptions = new List(); + } + + exceptions.Add(ex); + } + + return false; + } + } + + /// + public IDisposable BeginScope(TState state) + { + return NoOpDisposable.Instance; + } + + private static void ThrowLoggingError(List exceptions) + { + throw new AggregateException( + message: "An error occurred while writing to logger(s).", innerExceptions: exceptions); + } + } + + public class NoOpDisposable : IDisposable + { + public static NoOpDisposable Instance = new NoOpDisposable(); + + public void Dispose() { } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerFactory.cs b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerFactory.cs new file mode 100644 index 000000000000..030f0795a375 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerFactory.cs @@ -0,0 +1,150 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services +{ + /// + /// Produces instances of classes based on the given providers. + /// + public class WebAssemblyLoggerFactory : ILoggerFactory + { + private readonly Dictionary _loggers = new Dictionary(); + private readonly List _providerRegistrations = new List(); + private readonly object _sync = new object(); + private volatile bool _disposed; + + public WebAssemblyLoggerFactory() : this(Enumerable.Empty()) { } + + /// + /// Creates a new instance. + /// + /// The providers to use in producing instances. + public WebAssemblyLoggerFactory(IEnumerable providers) + { + foreach (var provider in providers) + { + AddProviderRegistration(provider, dispose: false); + } + } + + /// + /// Creates an with the given . + /// + /// The category name for messages produced by the logger. + /// The that was created. + public ILogger CreateLogger(string categoryName) + { + if (CheckDisposed()) + { + throw new ObjectDisposedException(nameof(WebAssemblyLoggerFactory)); + } + + lock (_sync) + { + if (!_loggers.TryGetValue(categoryName, out var logger)) + { + logger = new Logger + { + Loggers = CreateLoggers(categoryName), + }; + + _loggers[categoryName] = logger; + } + + return logger; + } + } + + /// + /// Adds the given provider to those used in creating instances. + /// + /// The to add. + public void AddProvider(ILoggerProvider provider) + { + if (CheckDisposed()) + { + throw new ObjectDisposedException(nameof(WebAssemblyLoggerFactory)); + } + + lock (_sync) + { + AddProviderRegistration(provider, dispose: true); + + foreach (var existingLogger in _loggers) + { + var logger = existingLogger.Value; + var WebAssemblyLoggerInformation = logger.Loggers; + + var newLoggerIndex = WebAssemblyLoggerInformation.Length; + Array.Resize(ref WebAssemblyLoggerInformation, WebAssemblyLoggerInformation.Length + 1); + WebAssemblyLoggerInformation[newLoggerIndex] = new WebAssemblyLoggerInformation(provider, existingLogger.Key); + + logger.Loggers = WebAssemblyLoggerInformation; + } + } + } + + private void AddProviderRegistration(ILoggerProvider provider, bool dispose) + { + _providerRegistrations.Add(new ProviderRegistration + { + Provider = provider, + ShouldDispose = dispose + }); + } + + private WebAssemblyLoggerInformation[] CreateLoggers(string categoryName) + { + var loggers = new WebAssemblyLoggerInformation[_providerRegistrations.Count]; + for (var i = 0; i < _providerRegistrations.Count; i++) + { + loggers[i] = new WebAssemblyLoggerInformation(_providerRegistrations[i].Provider, categoryName); + } + return loggers; + } + + /// + /// Check if the factory has been disposed. + /// + /// True when as been called + protected virtual bool CheckDisposed() => _disposed; + + /// + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + foreach (var registration in _providerRegistrations) + { + try + { + if (registration.ShouldDispose) + { + registration.Provider.Dispose(); + } + } + catch + { + // Swallow exceptions on dispose + } + } + } + } + + private struct ProviderRegistration + { + public ILoggerProvider Provider; + public bool ShouldDispose; + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerInformation.cs b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerInformation.cs new file mode 100644 index 000000000000..11c40cbef5e8 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/Logging/WebAssemblyLoggerInformation.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services +{ + internal readonly struct WebAssemblyLoggerInformation + { + public WebAssemblyLoggerInformation(ILoggerProvider provider, string category) : this() + { + ProviderType = provider.GetType(); + Logger = provider.CreateLogger(category); + Category = category; + } + + public ILogger Logger { get; } + + public string Category { get; } + + public Type ProviderType { get; } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyLoggerFactory.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyLoggerFactory.cs deleted file mode 100644 index 400d537fd0d1..000000000000 --- a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyLoggerFactory.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.JSInterop; - -namespace Microsoft.AspNetCore.Components.WebAssembly.Services -{ - internal class WebAssemblyLoggerFactory : ILoggerFactory - { - private readonly IJSInProcessRuntime _jsRuntime; - - public WebAssemblyLoggerFactory(IServiceProvider services) - { - _jsRuntime = (IJSInProcessRuntime)services.GetRequiredService(); - } - - // We might implement this in the future, but it's not required currently - public void AddProvider(ILoggerProvider provider) - => throw new NotSupportedException(); - - public ILogger CreateLogger(string categoryName) - => new WebAssemblyConsoleLogger(categoryName, _jsRuntime); - - public void Dispose() - { - // No-op - } - } -} diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/Logging/WebAssemblyLoggerFactoryTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/Logging/WebAssemblyLoggerFactoryTest.cs new file mode 100644 index 000000000000..fa827ded350c --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/Logging/WebAssemblyLoggerFactoryTest.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +using Moq; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services +{ + public class WebAssemblyLoggerFactoryTest + { + [Fact] + public void CreateLogger_ThrowsAfterDisposed() + { + // Arrange + var factory = new WebAssemblyLoggerFactory(); + + // Act + factory.Dispose(); + + // Assert + Assert.Throws(() => factory.CreateLogger("d")); + } + + [Fact] + public void AddProvider_ThrowsAfterDisposed() + { + // Arrange + var factory = new WebAssemblyLoggerFactory(); + var provider = new Mock(); + + // Act + factory.Dispose(); + + // Assert + Assert.Throws(() => ((ILoggerFactory)factory).AddProvider(provider.Object)); + } + + [Fact] + public void CanAddProviders() + { + // Arrange + var factory = new WebAssemblyLoggerFactory(); + var provider1 = new Mock(); + var provider2 = new Mock(); + + // Act + var exception1 = Record.Exception(() => factory.AddProvider(provider1.Object)); + var exception2 = Record.Exception(() => factory.AddProvider(provider2.Object)); + + // Assert + Assert.Null(exception1); + Assert.Null(exception2); + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs index e35d0a2955d9..9efe23486873 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -218,7 +218,6 @@ private static IReadOnlyList DefaultServiceTypes typeof(NavigationManager), typeof(INavigationInterception), typeof(ILoggerFactory), - typeof(ILogger<>), typeof(IWebAssemblyHostEnvironment), }; } diff --git a/src/Components/test/testassets/BasicTestApp/PrependMessageLoggerFactory.cs b/src/Components/test/testassets/BasicTestApp/PrependMessageLoggerProvider.cs similarity index 63% rename from src/Components/test/testassets/BasicTestApp/PrependMessageLoggerFactory.cs rename to src/Components/test/testassets/BasicTestApp/PrependMessageLoggerProvider.cs index 40014d0ca43c..2867facb2cce 100644 --- a/src/Components/test/testassets/BasicTestApp/PrependMessageLoggerFactory.cs +++ b/src/Components/test/testassets/BasicTestApp/PrependMessageLoggerProvider.cs @@ -3,6 +3,9 @@ using System; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Components.WebAssembly.Services; +using Microsoft.JSInterop; namespace BasicTestApp { @@ -11,25 +14,37 @@ namespace BasicTestApp // However, it's valuable to pass through all calls to the default implementation too // so that if any defect in the underlying implementation would break tests, we still see it. - public class PrependMessageLoggerFactory : ILoggerFactory + public class PrependMessageLoggerProvider: ILoggerProvider { - private readonly string _message; - private readonly ILoggerFactory _underlyingFactory; + ILogger _logger; + IConfiguration _configuration; + ILogger _defaultLogger; + private bool _disposed = false; - public PrependMessageLoggerFactory(string message, ILoggerFactory underlyingFactory) + public PrependMessageLoggerProvider(IConfiguration configuration, IJSRuntime runtime) { - _message = message; - _underlyingFactory = underlyingFactory; + _configuration = configuration; + _defaultLogger = new WebAssemblyConsoleLogger(runtime); } - public void AddProvider(ILoggerProvider provider) - => _underlyingFactory.AddProvider(provider); - public ILogger CreateLogger(string categoryName) - => new PrependMessageLogger(_message, _underlyingFactory.CreateLogger(categoryName)); + { + if (_logger == null) + { + var message = _configuration["Logging:PrependMessage:Message"]; + _logger = new PrependMessageLogger(message, _defaultLogger); + } + return _logger; + } public void Dispose() - => _underlyingFactory.Dispose(); + { + if (!_disposed) + { + _logger = null; + } + _disposed = true; + } private class PrependMessageLogger : ILogger { diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index d0b7dadd1e93..46d65abe2e8f 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; @@ -14,8 +15,10 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Http; using Microsoft.AspNetCore.Components.WebAssembly.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.JSInterop; namespace BasicTestApp { @@ -44,19 +47,22 @@ public static async Task Main(string[] args) policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); }); - // Replace the default logger with a custom one that wraps it - var originalLoggerDescriptor = builder.Services.Single(d => d.ServiceType == typeof(ILoggerFactory)); - builder.Services.AddSingleton(services => + + var inMemoryConfiguration = new Dictionary { - var originalLogger = (ILoggerFactory)Activator.CreateInstance( - originalLoggerDescriptor.ImplementationType, - new object[] { services }); - return new PrependMessageLoggerFactory("Custom logger", originalLogger); - }); + ["Logging:PrependMessage:Message"] = "Custom logger" + }; + builder.Configuration.AddInMemoryCollection(inMemoryConfiguration); var host = builder.Build(); + ConfigureCulture(host); + var loggerFactory = host.Services.GetService(); + var configuration = host.Services.GetService(); + var runtime = host.Services.GetService(); + loggerFactory.AddProvider(new PrependMessageLoggerProvider(configuration, runtime)); + await host.RunAsync(); }