EF Core Connection to Azure SQL with Managed Identity

前端 未结 4 1271
日久生厌
日久生厌 2020-12-28 13:35

I am using EF Core to connect to a Azure SQL Database deployed to Azure App Services. I am using an access token (obtained via the Managed Identities) to connect to Azure SQ

相关标签:
4条回答
  • 2020-12-28 14:15

    For developers using .NET Framework for Managed Identity, the below code might be helpful for getting the entity connection:

    app.config:

    <add key="ResourceId" value="https://database.windows.net/" />
    <add key="Con" value="data source=tcp:sampledbserver.database.windows.net,1433;initial catalog=sampledb;MultipleActiveResultSets=True;Connect Timeout=30;" />
    

    c# file

    using System;
    using System.Configuration;
    using System.Data.Entity.Core.EntityClient;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.SqlClient;
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using Microsoft.Azure.Services.AppAuthentication;
    
    public static EntityConnection GetEntityConnectionString()
    {
        MetadataWorkspace workspace = new MetadataWorkspace(
           new string[] { "res://*/" },
           new Assembly[] { Assembly.GetExecutingAssembly() });
    
        SqlConnection sqlConnection = new SqlConnection(Con);
    
        var result = (new AzureServiceTokenProvider()).GetAccessTokenAsync(ResourceId).Result;
    
        sqlConnection.AccessToken = result ?? throw new InvalidOperationException("Failed to obtain the access token");
    
        EntityConnection entityConnection = new EntityConnection(
            workspace,
            sqlConnection);
    
        return entityConnection;
    }
    
    0 讨论(0)
  • 2020-12-28 14:22

    While the approach is generally correct in the sense that there is no other way than having to write custom code that sets the AccessToken of the connection, there is a couple of issues in your implementation that could be avoided by using a DbConnectionInterceptor as I will describe below. Those two issues are:

    1. You took the responsibility of creating the connection object yourself. But you don't dispose it. Disposal will be tricky in your implementation, and that's why you might have skipped it.
    2. Your code is blocking, which wastes precious CPU time, since you use .Result to block while waiting for the access token.

    A better alternative is to use interceptors, which EF Core supports. You will start with a DbContext like this:

    public class MyCustomDbContextFactory : IMyCustomDbContextFactory
    {
        private readonly string _connectionString;
        private readonly AzureAuthenticationInterceptor _azureAuthenticationInterceptor;
        public MyCustomDbContextFactory(DbContextFactoryOptions options, AzureAuthenticationInterceptor azureAuthenticationInterceptor)
        {
            _connectionString = options.ConnectionString;
            _azureAuthenticationInterceptor = azureAuthenticationInterceptor;
        }
        public MyCustomDbContext Create()
        {
            var optionsBuilder = new DbContextOptionsBuilder<MyCustomDbContext>();
            optionsBuilder
                .UseSqlServer(_connectionString)
                .AddInterceptors(_azureAuthenticationInterceptor);
            return new MyCustomDbContext(optionsBuilder.Options);
        }
    }
    

    And this is the interceptor implementation:

    public class AzureAuthenticationInterceptor : DbConnectionInterceptor
    {
        private const string AzureDatabaseResourceIdentifier = "https://database.windows.net";
        private readonly AzureServiceTokenProvider _azureServiceTokenProvider;
        public AzureAuthenticationInterceptor(AzureServiceTokenProvider azureServiceTokenProvider) : base()
        {
            _azureServiceTokenProvider = azureServiceTokenProvider;
        }
        public override async Task<InterceptionResult> ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
        {
            if (connection is SqlConnection sqlConnection)
            {
                sqlConnection.AccessToken = await GetAccessToken();
            }
            return result;
        }
        public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
        {
            if (connection is SqlConnection sqlConnection)
            {
                sqlConnection.AccessToken = GetAccessToken().Result;
            }
            return result;
        }
        private Task<string> GetAccessToken() => _azureServiceTokenProvider.GetAccessTokenAsync(AzureDatabaseResourceIdentifier);
    }
    

    And this is how to configure your services:

    services.AddSingleton(new DbContextFactoryOptions(connection_string));
    services.AddSingleton(new AzureAuthenticationInterceptor(new AzureServiceTokenProvider()));
    

    And finally, this is how to instantiate DbContext objects in your repository:

    public async Task<IEnumerable<MyCustomEntity>> GetAll()
    {
    using var context = _notificationsDbContextFactory.Create();  // Injected in ctor
    var dbos = await context.MyCustomEntity.ToListAsync();
    return ... // something;
    }
    
    0 讨论(0)
  • 2020-12-28 14:28

    Is this a right way to do it or will it have issues with performance?

    That is the right way. OnConfiguring is called for each new DbContext, so assuming you don't have any long-lived DbContext instances, this is the right pattern.

    Do I need to worry about token expiration? I am not caching the token as of now.

    AzureServiceTokenProvider takes care of caching.

    Does EF Core has any better way to handle this?

    Setting the SqlConnection.AccessToken is currently the only way of using AAD Auth in SqlClient for .NET Core.

    0 讨论(0)
  • 2020-12-28 14:33

    For those who still fall on the same problem, I've solved the problem by using a DbInterceptor so I can asynchronously get the token without blocking the application. I had opened an issue on EF Core repo but I've closed with the solution:

    https://github.com/dotnet/efcore/issues/21043

    I hope it help.

    0 讨论(0)
提交回复
热议问题