Skip to content
This repository was archived by the owner on Oct 17, 2018. It is now read-only.

Added Entity Framework Core backed IXmlRepository with tests and sample #303

Merged
merged 19 commits into from
Aug 29, 2018

Conversation

dansward
Copy link
Contributor

@dansward dansward commented Apr 2, 2018

No description provided.

@dnfclas
Copy link

dnfclas commented Apr 2, 2018

CLA assistant check
All CLA requirements met.

@Eilon
Copy link
Member

Eilon commented Apr 2, 2018

@dansward is there an issue associated with this PR that describes the scenario? If not, could you open a new issue at https://github.com/aspnet/Home/issues and describe the details? Thanks!

@dansward
Copy link
Contributor Author

dansward commented Apr 2, 2018

@Eilon A related issue exists. Create SQL XmlRepository #2505, should I create one specifically for EF or will that one work?

@Eilon
Copy link
Member

Eilon commented Apr 2, 2018

@dansward I think that's probably sufficient. I'll let the experts in this space evaluate the submission. Thanks!

Tagging @muratg @blowdart to evaluate.

namespace Microsoft.AspNetCore.DataProtection
{
/// <summary>
/// Contains Redis-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change mention of Redis :)

/// <summary>
/// Code first model used by <see cref="EntityFrameworkCoreXmlRepository"/>.
/// </summary>
public class Key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is never going to be typed in full by the user anywhere a more specific name would be better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataProtectionKey? Should the DbContext class be renamed to match as well, DataProtectionKeyStore?

@blowdart
Copy link
Member

blowdart commented Apr 2, 2018

And adding @natemcmaster

Daniel Ward added 2 commits April 2, 2018 13:21
Fixed comment on extension method: replaced Redis with EntityFrameworkCore
@natemcmaster
Copy link
Contributor

Hey @dansward, thanks for taking an initial stab at this. We haven't yet nailed down a design for the API and expected usage of storing keys in a SQL database. We're going to take a look at dotnet/aspnetcore#2505 in our triage on Friday, and I'll get back to you then.

…tion more natural.

Updated EFCore sample and tests to use new extension method.
@natemcmaster
Copy link
Contributor

We discussed this in triage. First of all, thanks. We've been thinking of doing dotnet/aspnetcore#2505 for a while. This was a good nudge to start the conversation again.

We're going to leave this PR open for now for two reasons.

  1. This feature isn't something we can commit to shipping the 2.1.0 timeframe. We want to leave enough time for due diligence on the design and testing, and 2.1.0 is locking down soon.
  2. We want to make sure the design of the API satisfies a few requirements that I don't think this currently implementation has. Those requirements are a bit vague still -- we are going to have a conversation with the EF Core team first -- but when we have them, I'll list those requirements in Create SQL XmlRepository for storing Data Protection keys dotnet/aspnetcore#2505. If you're open to working with us on that design and willing to update this PR to meet those, we may be able to take this PR as the starting point for the feature.

Thanks,
Nate

@dansward
Copy link
Contributor Author

dansward commented Apr 6, 2018

@natemcmaster Sounds good. I'll wait for the update to aspnet/Home#2505.

@ekarlso
Copy link

ekarlso commented Jun 24, 2018

Any way I can get migrations working with this?

I tried to do:

                .PersistKeysToEntityFrameworkCore(options =>
                {
                    options.KeyStoreOptionsBuilderAction = builder =>
                    {
                        var dpapiMigrationsAssembly = typeof(KeyStore).GetTypeInfo().Assembly.GetName().Name;
                        builder.UseNpgsql(idsrvKeysSqlConnectionString, b => b.MigrationsAssembly(dpapiMigrationsAssembly));
                    };
                })```

@ekarlso
Copy link

ekarlso commented Jun 26, 2018

Anyone wanna play in on this? @dansward

@dansward
Copy link
Contributor Author

dansward commented Jun 27, 2018 via email

@natemcmaster natemcmaster changed the base branch from dev to master July 2, 2018 17:57
@natemcmaster
Copy link
Contributor

Hey @dansward, just wanted to let you know I haven't forgotten about this. Thanks for being patient. Our master branch is open for 3.0 work now. DataProtection is one of the dozens of projects I have to find time to support, so I appreciate your help.

What are your thoughts on this comment? dotnet/aspnetcore#2505 (comment). I think @ekarlso's comment pointed out one of the things we should discuss more - how can we help users create/manage the database to back this repository?

@natemcmaster
Copy link
Contributor

Just tossing out an idea. One way I thought we might implement is by providing an API like this:

    public static class EtityFrameworkDataProtectionExtensions
    {
        public static IDataProtectionBuilder PersistKeysToDbContext<TContext>(this IDataProtectionBuilder builder)
            where TContext: DbContext, IDataProtectionKeysDbSet
        {
            /// implementation ...
            return builder;
        }
    }

    public interface IDataProtectionKeysDbSet
    {
        DbSet<DataProtectionKeys> DataProtectionKeys { get; }
    }

    public class DataProtectionKeys
    {
        public long Id { get; set; }
        public string Content { get; set; }
    }

I would have to play with an implementation, but my thinking here is that this gives devs the option to use their existing DbContext type and just add a new table, but also gives them flexibility to keep keys in a separate DbContext if they wish.

Also, we can create the DbContext instance through dependency injection, so devs only have to configure their connection strings in EF Core once

@dansward
Copy link
Contributor Author

dansward commented Jul 9, 2018

@natemcmaster, that worked really well. Let me know if this meets the requirements.

@natemcmaster natemcmaster changed the base branch from master to release/2.2 July 11, 2018 23:51
Copy link
Contributor

@natemcmaster natemcmaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@divega @bricelam can you also take a look at this? I want someone from EF to take a look, too. My memory is a bit rusty on some the EF Core workings, so want to make sure what I've said below is still accurate.

@dansward - this is a really good start. I like the API shape and I think this is headed in the right direction. My comments are mostly about implementation details now. Also, I've updated your PR to target release/2.2 since the master branch now targets 3.0, which won't ship for a while.

Thanks!
Nate

/// <summary>
/// The friendly name of the <see cref="DataProtectionKey"/>.
/// </summary>
[Key]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current contract for IXmlRepository specifies that friendly name is optional. If specified, it should be unique, but it may be null. We should probably add a property instead, such as public int Id { get; set; }.

@divega can you refresh my memory - do EF model conventions apply to internal properties?

{
builder.Services.Configure<KeyManagementOptions>(options =>
{
options.XmlRepository = new EntityFrameworkCoreXmlRepository<TContext>(builder.Services.BuildServiceProvider().GetRequiredService<TContext>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may cause problems with scope. Typically, DbContext is a scoped service, but this would required a DbContext to be a singleton.


/// <inheritdoc />
public IReadOnlyCollection<XElement> GetAllElements()
=> _context.DataProtectionKeys.Select(key => XElement.Parse(key.Xml)).ToList().AsReadOnly();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be gentle and use TryParse instead. If the database has malformed xml, we should skip loading that key and log a warning.

Also, let's make this a notracking query. We don't need to make changes to the entities.

/// Creates a new instance of the <see cref="EntityFrameworkCoreXmlRepository{TContext}"/>.
/// </summary>
/// <param name="context">The context used to store instances of <see cref="DataProtectionKey"/></param>
public EntityFrameworkCoreXmlRepository(TContext context) => _context = context ?? throw new ArgumentNullException(nameof(context));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of taking an instance of TContext, we should take a service which can construct a new instance of the DbContext. In earlier versions of EFCore, there was an IDbContextFactory<TContext> service you could use. I don't remember if that is still around in EF Core 2.0 (cc @divega).

Also, EntityFrameworkCoreXmlRepository is a singleton, but each time it invokes an interaction with the database, we should create a new instance of DbContext.

Copy link
Contributor

@bricelam bricelam Jul 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use IDbContextFactory. It was only for design time. We renamed it in 2.0 to reflect that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When are keys retrieved? Once during app start? During a request?

If it's just once, just creating a scope here might be best.

{
_context.DataProtectionKeys.Add(newKey);
}
_context.SaveChanges();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a logging statement to this method before you call SaveChanges? It only needs to be LogLevel.Debug that says something like "Saving key {friendlyName} to {dbcontextname}".

@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nit: can you apply the standard file header to all .cs files?

.AddDbContext<DataProtectionKeyContext>()
.AddDataProtection()
.PersistKeysToDbContext<DataProtectionKeyContext>();
var serviceProvider = serviceCollection.BuildServiceProvider();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to .BuildServiceProvider(validateScopes: true)? This typically helps us find issues in the CI services.

public interface IDataProtectionKeyContext
{
/// <inheritdoc />
DbSet<DataProtectionKey> DataProtectionKeys { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring a DbSet<DataProtectionKey> on the DbContext seems a bit rigid. Could just use DbContext.Set<DataProtectionKey>() instead.

/// </summary>
public interface IDataProtectionKeyContext
{
/// <inheritdoc />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherit docs from where? 😉

Added exception handling and logging for malformed key xml
Changed to NoTracking queries
Added logging when saving new key
Added standard file headers
@dansward
Copy link
Contributor Author

@natemcmaster @bricelam
I made most of the changes requested. I did not remove the interface but am using DbContext.Set in the repository.

I'm not sure how to fix the conflicts. I hate to waste your time but any help would be appreciated.

@natemcmaster natemcmaster changed the base branch from release/2.2 to feature/efcore-provider August 29, 2018 20:56
@natemcmaster
Copy link
Contributor

Thanks for doing the initial legwork to get this going @dansward. I've pushed a commit to your branch to resolve the conflicts. I'm going to merge this in to a staging branch while I do a few more tweaks. Assuming I don't run into other issues, this will be in the 2.2 Preview 2 release.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants