-
Notifications
You must be signed in to change notification settings - Fork 720
Add Pomelo.EntityFrameworkCore.MySql component #1161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cca920b
a0e6c0d
75f1b1f
d94b366
e46b6d5
b932eb2
58c86ce
d41e043
5ab2474
5fdfef5
4712079
953bb3f
e6101a9
2623f93
f133452
cdb6909
59f9952
4d9913d
a73e4fe
db875a4
823722c
bc605cf
817940b
ab0419a
0964125
079c30e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>$(NetCurrent)</TargetFramework> | ||
| <IsPackable>true</IsPackable> | ||
| <PackageTags>$(ComponentEfCorePackageTags) pomelo mysql sql</PackageTags> | ||
| <Description>A MySQL provider for Entity Framework Core that integrates with Aspire, including connection pooling, health checks, logging, and telemetry.</Description> | ||
| <PackageIconFullPath>$(SharedDir)SQL_256x.png</PackageIconFullPath> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Compile Include="..\Common\ConfigurationSchemaAttributes.cs" Link="ConfigurationSchemaAttributes.cs" /> | ||
| <Compile Include="..\Common\HealthChecksExtensions.cs" Link="HealthChecksExtensions.cs" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> | ||
| <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" /> | ||
| <PackageReference Include="MySqlConnector.Logging.Microsoft.Extensions.Logging" /> | ||
| <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" /> | ||
| <PackageReference Include="OpenTelemetry.Extensions.Hosting" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.EventCounters" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| using Aspire; | ||
| using Aspire.Pomelo.EntityFrameworkCore.MySql; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using MySqlConnector.Logging; | ||
| using OpenTelemetry.Metrics; | ||
|
|
||
| namespace Microsoft.Extensions.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for registering a MySQL database context in an Aspire application. | ||
| /// </summary> | ||
| public static partial class AspireEFMySqlExtensions | ||
| { | ||
| private const string DefaultConfigSectionName = "Aspire:Pomelo:EntityFrameworkCore:MySql"; | ||
| private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; | ||
|
|
||
| /// <summary> | ||
| /// Registers the given <see cref="DbContext" /> as a service in the services provided by the <paramref name="builder"/>. | ||
| /// Enables db context pooling, corresponding health check, logging and telemetry. | ||
| /// </summary> | ||
| /// <typeparam name="TContext">The <see cref="DbContext" /> that needs to be registered.</typeparam> | ||
| /// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param> | ||
| /// <param name="connectionName">A name used to retrieve the connection string from the ConnectionStrings configuration section.</param> | ||
| /// <param name="configureSettings">An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.</param> | ||
| /// <param name="configureDbContextOptions">An optional delegate to configure the <see cref="DbContextOptions"/> for the context.</param> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Reads the configuration from "Aspire:Pomelo:EntityFrameworkCore:MySql:{typeof(TContext).Name}" config section, or "Aspire:Pomelo:EntityFrameworkCore:MySql" if former does not exist. | ||
| /// </para> | ||
| /// <para> | ||
| /// The <see cref="DbContext.OnConfiguring" /> method can then be overridden to configure <see cref="DbContext" /> options. | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception> | ||
| /// <exception cref="InvalidOperationException">Thrown when mandatory <see cref="PomeloEntityFrameworkCoreMySqlSettings.ConnectionString"/> is not provided.</exception> | ||
| public static void AddMySqlDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( | ||
| this IHostApplicationBuilder builder, | ||
| string connectionName, | ||
| Action<PomeloEntityFrameworkCoreMySqlSettings>? configureSettings = null, | ||
| Action<DbContextOptionsBuilder>? configureDbContextOptions = null) where TContext : DbContext | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| PomeloEntityFrameworkCoreMySqlSettings settings = new(); | ||
| var typeSpecificSectionName = $"{DefaultConfigSectionName}:{typeof(TContext).Name}"; | ||
| var typeSpecificConfigurationSection = builder.Configuration.GetSection(typeSpecificSectionName); | ||
| if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380 | ||
| { | ||
| typeSpecificConfigurationSection.Bind(settings); | ||
| } | ||
| else | ||
| { | ||
| builder.Configuration.GetSection(DefaultConfigSectionName).Bind(settings); | ||
| } | ||
|
|
||
| if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) | ||
| { | ||
| settings.ConnectionString = connectionString; | ||
| } | ||
|
|
||
| configureSettings?.Invoke(settings); | ||
|
|
||
| if (settings.DbContextPooling) | ||
| { | ||
| builder.Services.AddDbContextPool<TContext>(ConfigureDbContext); | ||
| } | ||
| else | ||
| { | ||
| builder.Services.AddDbContext<TContext>(ConfigureDbContext); | ||
| } | ||
|
|
||
| if (settings.HealthChecks) | ||
| { | ||
| // calling MapHealthChecks is the responsibility of the app, not Component | ||
| builder.TryAddHealthCheck( | ||
| name: typeof(TContext).Name, | ||
| static hcBuilder => hcBuilder.AddDbContextCheck<TContext>()); | ||
| } | ||
|
|
||
| if (settings.Tracing) | ||
| { | ||
| builder.Services.AddOpenTelemetry() | ||
| .WithTracing(tracerProviderBuilder => | ||
| { | ||
| // add tracing from the underlying MySqlConnector ADO.NET library | ||
| tracerProviderBuilder.AddSource("MySqlConnector"); | ||
| }); | ||
| } | ||
|
|
||
| if (settings.Metrics) | ||
| { | ||
| builder.Services.AddOpenTelemetry() | ||
| .WithMetrics(meterProviderBuilder => | ||
| { | ||
| // Currently EF provides only Event Counters: | ||
| // https://learn.microsoft.com/ef/core/logging-events-diagnostics/event-counters?tabs=windows#counters-and-their-meaning | ||
| meterProviderBuilder.AddEventCountersInstrumentation(eventCountersInstrumentationOptions => | ||
| { | ||
| // The magic strings come from: | ||
| // https://github.com/dotnet/efcore/blob/a1cd4f45aa18314bc91d2b9ea1f71a3b7d5bf636/src/EFCore/Infrastructure/EntityFrameworkEventSource.cs#L45 | ||
| eventCountersInstrumentationOptions.AddEventSources("Microsoft.EntityFrameworkCore"); | ||
| }); | ||
|
|
||
| // add metrics from the underlying MySqlConnector ADO.NET library | ||
| meterProviderBuilder.AddMeter("MySqlConnector"); | ||
| }); | ||
| } | ||
|
|
||
| void ConfigureDbContext(IServiceProvider serviceProvider, DbContextOptionsBuilder dbContextOptionsBuilder) | ||
| { | ||
| // use the legacy method of setting the ILoggerFactory because Pomelo EF Core doesn't use MySqlDataSource | ||
| if (serviceProvider.GetService<ILoggerFactory>() is { } loggerFactory) | ||
| { | ||
| MySqlConnectorLogManager.Provider = new MicrosoftExtensionsLoggingLoggerProvider(loggerFactory); | ||
| } | ||
|
|
||
| var connectionString = settings.ConnectionString ?? string.Empty; | ||
|
|
||
| ServerVersion serverVersion; | ||
| if (settings.ServerVersion is null) | ||
| { | ||
| if (string.IsNullOrEmpty(connectionString)) | ||
| { | ||
| ThrowForMissingConnectionString(); | ||
| } | ||
| serverVersion = ServerVersion.AutoDetect(connectionString); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Playing with this change locally, I'm a bit concerned about this now. I'm trying to run the integration tests (which aren't run in CI yet - we are working on that). If the Pomelo test is run by itself, it fails because just trying to get the DbContext out of DI is trying to establish a connection. But the MySql docker container isn't up yet (it takes a while to get up) and this line is throwing because it can't make a connection. I wonder if we should retry this up to the MaxRetryCount. It isn't a great experience to fail getting the DbContext because it happens as part of ASP.NET calling into the user's code. So there's no chance for them to retry. Thoughts? Maybe this shouldn't block the initial check-in, since you can work around the issue by setting ServerVersion explicitly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
And then still fail with a fatal error if it can't make a connection? (As opposed to making up a value like That definitely seems like an improvement over the status quo to me. (Unless of course 100% of the time it still fails but now just takes longer.) In my original PR (before 2623f93), this was a mandatory option that had to be supplied by the user. That was less user-friendly on one hand, but maybe more predictable? Very random thought (and may be going completely down the wrong path): is there some "magic" we could do by detecting if there's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's discuss the options in a new issue explicitly for this. Can you open it? or do you want me to? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| else | ||
| { | ||
| serverVersion = ServerVersion.Parse(settings.ServerVersion); | ||
| } | ||
|
|
||
| var builder = dbContextOptionsBuilder.UseMySql(connectionString, serverVersion, builder => | ||
| { | ||
| // delay validating the ConnectionString until the DbContext is configured. This ensures an exception doesn't happen until a Logger is established. | ||
| if (string.IsNullOrEmpty(connectionString)) | ||
| { | ||
| ThrowForMissingConnectionString(); | ||
| } | ||
|
|
||
| // Resiliency: | ||
| // 1. Connection resiliency automatically retries failed database commands: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/wiki/Configuration-Options#enableretryonfailure | ||
| if (settings.MaxRetryCount > 0) | ||
| { | ||
| builder.EnableRetryOnFailure(settings.MaxRetryCount); | ||
| } | ||
| }); | ||
|
|
||
| configureDbContextOptions?.Invoke(dbContextOptionsBuilder); | ||
|
|
||
| void ThrowForMissingConnectionString() | ||
| { | ||
| throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section."); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire; | ||
| using Aspire.Pomelo.EntityFrameworkCore.MySql; | ||
|
|
||
| [assembly: ConfigurationSchema("Aspire:Pomelo:EntityFrameworkCore:MySql", typeof(PomeloEntityFrameworkCoreMySqlSettings))] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also add all the logging categories that are in Telemetry.md. That way they light up in the appsettings.json file intellisense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I shouldn't have duplicated the MySqlConnector logging categories into the Pomelo section in Telemetry.md. (I assume they will ultimately get pulled in through being a transitive dependency?) I'll remove them from Telemetry.md. |
||
|
|
||
| [assembly: LoggingCategories( | ||
| "Microsoft.EntityFrameworkCore", | ||
| "Microsoft.EntityFrameworkCore.ChangeTracking", | ||
| "Microsoft.EntityFrameworkCore.Database", | ||
| "Microsoft.EntityFrameworkCore.Database.Command", | ||
| "Microsoft.EntityFrameworkCore.Database.Connection", | ||
| "Microsoft.EntityFrameworkCore.Database.Transaction", | ||
| "Microsoft.EntityFrameworkCore.Infrastructure", | ||
| "Microsoft.EntityFrameworkCore.Migrations", | ||
| "Microsoft.EntityFrameworkCore.Model", | ||
| "Microsoft.EntityFrameworkCore.Model.Validation", | ||
| "Microsoft.EntityFrameworkCore.Query", | ||
| "Microsoft.EntityFrameworkCore.Update")] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| { | ||
bgrainger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "definitions": { | ||
| "logLevel": { | ||
| "properties": { | ||
| "Microsoft.EntityFrameworkCore": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.ChangeTracking": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Database": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Database.Command": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Database.Connection": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Database.Transaction": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Infrastructure": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Migrations": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Model": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Model.Validation": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Query": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Update": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "properties": { | ||
| "Aspire": { | ||
| "type": "object", | ||
| "properties": { | ||
| "Pomelo": { | ||
| "type": "object", | ||
| "properties": { | ||
| "EntityFrameworkCore": { | ||
| "type": "object", | ||
| "properties": { | ||
| "MySql": { | ||
| "type": "object", | ||
| "properties": { | ||
| "ConnectionString": { | ||
| "type": "string", | ||
| "description": "Gets or sets the connection string of the MySQL database to connect to." | ||
| }, | ||
| "DbContextPooling": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the DbContext will be pooled or explicitly created every time it's requested.", | ||
| "default": true | ||
| }, | ||
| "HealthChecks": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not.", | ||
| "default": true | ||
| }, | ||
| "MaxRetryCount": { | ||
| "type": "integer", | ||
| "description": "Gets or sets the maximum number of retry attempts." | ||
| }, | ||
| "Metrics": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.", | ||
| "default": true | ||
| }, | ||
| "ServerVersion": { | ||
| "type": "string", | ||
| "description": "Gets or sets the server version of the MySQL database to connect to." | ||
| }, | ||
| "Tracing": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.", | ||
| "default": true | ||
| } | ||
| }, | ||
| "description": "Provides the client configuration settings for connecting to a MySQL database using EntityFrameworkCore." | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "type": "object" | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.