-
Notifications
You must be signed in to change notification settings - Fork 931
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
cca920b
Add Pomelo.EntityFrameworkCore.MySql component.
bgrainger a0e6c0d
Add Pomelo.EntityFrameworkCore.MySql tests.
bgrainger 75f1b1f
Merge main into pomelo.
bgrainger d94b366
Remove incorrect documentation.
bgrainger e46b6d5
Remove irrelevant TODO comments.
bgrainger b932eb2
Update XML documentation.
bgrainger 58c86ce
Update XML documentation to current conventions.
bgrainger d41e043
Add documentation for Pomelo metrics.
bgrainger 5ab2474
Change catch block to the exception Pomelo throws.
bgrainger 5fdfef5
Merge main into pomelo.
bgrainger 4712079
Reuse existing TestDbContext.cs file.
bgrainger 953bb3f
Use configuration schema generator.
bgrainger e6101a9
Remove "Container" from AddMySql.
bgrainger 2623f93
Allow ServerVersion setting to be optional.
bgrainger f133452
Remove (now optional) ServerVersion setting from test.
bgrainger cdb6909
Merge main into pomelo.
bgrainger 59f9952
Fix bad merge.
bgrainger 4d9913d
Remove MySqlConnector logging categories from Pomelo section.
bgrainger a73e4fe
Delete meaningless comment.
bgrainger db875a4
Add Microsoft.Extensions.Configuration.Binder.
bgrainger 823722c
Revert "Remove (now optional) ServerVersion setting from test."
bgrainger bc605cf
Add basic MySQL Aspire.Hosting tests.
bgrainger 817940b
Add Pomelo EFCore integration tests.
bgrainger ab0419a
Merge main into pomelo.
bgrainger 0964125
Incorporate project change from dff1467f.
bgrainger 079c30e
PR feedback
eerhardt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
...ts/Aspire.Pomelo.EntityFrameworkCore.MySql/Aspire.Pomelo.EntityFrameworkCore.MySql.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <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\HealthChecksExtensions.cs" Link="HealthChecksExtensions.cs" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <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> | ||
163 changes: 163 additions & 0 deletions
163
src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| // 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> | ||
| /// This method does not add any telemetry exporters. | ||
| /// </para> | ||
| /// <para> | ||
| /// The <see cref="DbContext.OnConfiguring" /> method can then be overridden to configure <see cref="DbContext" /> options. | ||
| /// </para> | ||
| /// <para> | ||
| /// The database developer page exception filter is registered only in the Development environment. | ||
| /// It also requires an extra call to UseDeveloperExceptionPage to work. | ||
| /// </para> | ||
|
bgrainger marked this conversation as resolved.
Outdated
|
||
| /// </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"); | ||
|
|
||
| // TODO: add any Pomelo EF Core tracing | ||
| // We don't need to enable it for EF via OpenTelemetry.Instrumentation.EntityFrameworkCore. | ||
| }); | ||
| } | ||
|
|
||
| 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"); | ||
|
|
||
| // TODO: add any Pomelo EF Core metrics | ||
|
bgrainger marked this conversation as resolved.
Outdated
|
||
| }); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| if (settings.ServerVersion is null) | ||
| { | ||
| throw new InvalidOperationException($"ServerVersion is missing. It should be provided in the ServerVersion' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section, or set by the 'configureSettings' callback."); | ||
| } | ||
|
bgrainger marked this conversation as resolved.
|
||
|
|
||
| // We don't register logger factory, because there is no need to: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks | ||
| var connectionString = settings.ConnectionString ?? string.Empty; | ||
| var 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)) | ||
| { | ||
| throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section."); | ||
| } | ||
|
|
||
| // 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); | ||
| } | ||
| } | ||
| } | ||
100 changes: 100 additions & 0 deletions
100
src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/ConfigurationSchema.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| { | ||
|
bgrainger marked this conversation as 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.Transaction": { | ||
| "$ref": "#/definitions/logLevelThreshold" | ||
| }, | ||
| "Microsoft.EntityFrameworkCore.Database.Connection": { | ||
| "$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." | ||
| }, | ||
| "ServerVersion": { | ||
| "type": "string", | ||
| "description": "Gets or sets the server version 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 | ||
| }, | ||
| "MaxRetryCount": { | ||
| "type": "integer", | ||
| "description": "Gets or sets the maximum number of retry attempts. Set it to 0 to disable the retry mechanism.", | ||
| "default": 6 | ||
| }, | ||
| "HealthChecks": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not.", | ||
| "default": true | ||
| }, | ||
| "Tracing": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.", | ||
| "default": true | ||
| }, | ||
| "Metrics": { | ||
| "type": "boolean", | ||
| "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.", | ||
| "default": true | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "type": "object" | ||
| } | ||
51 changes: 51 additions & 0 deletions
51
...ponents/Aspire.Pomelo.EntityFrameworkCore.MySql/PomeloEntityFrameworkCoreMySqlSettings.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Aspire.Pomelo.EntityFrameworkCore.MySql; | ||
|
|
||
| /// <summary> | ||
| /// Provides the client configuration settings for connecting to a MySQL database using EntityFrameworkCore. | ||
| /// </summary> | ||
| public sealed class PomeloEntityFrameworkCoreMySqlSettings | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the connection string of the MySQL database to connect to. | ||
| /// </summary> | ||
| public string? ConnectionString { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the server version of the MySQL database to connect to. | ||
| /// </summary> | ||
| public string? ServerVersion { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// <para>Gets or sets a boolean value that indicates whether the DbContext will be pooled or explicitly created every time it's requested.</para> | ||
| /// <para>Enabled by default.</para> | ||
| /// </summary> | ||
|
bgrainger marked this conversation as resolved.
|
||
| /// <remarks>Should be set to false in multi-tenant scenarios.</remarks> | ||
| public bool DbContextPooling { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// <para>Gets or sets the maximum number of retry attempts.</para> | ||
| /// <para>Default value is 6, set it to 0 to disable the retry mechanism.</para> | ||
| /// </summary> | ||
| public int MaxRetryCount { get; set; } = 6; | ||
|
|
||
| /// <summary> | ||
| /// <para>Gets or sets a boolean value that indicates whether the database health check is enabled or not.</para> | ||
| /// <para>Enabled by default.</para> | ||
| /// </summary> | ||
| public bool HealthChecks { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// <para>Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.</para> | ||
| /// <para>Enabled by default.</para> | ||
| /// </summary> | ||
| public bool Tracing { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// <para>Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.</para> | ||
| /// <para>Enabled by default.</para> | ||
| /// </summary> | ||
| public bool Metrics { get; set; } = true; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.