Entity Framework Core UI plugin for JetBrains Rider

repository·main·Indexed 41 days ago

https://github.com/jetbrains/rider-efcore

A visual interface for common EF Core commands within JetBrains Rider, including migration management, database scaffolding, and SQL script generation. Bundled starting from Rider 2023.3, it supports .NET SDKs and EF Core tools version 5.0 or higher across target frameworks from netcoreapp3.1 to net10.0.

Tokens
5.3K
Snippets
12
Records
24
Agent score
52%

What's inside EF Core UI

  1. Available Entity Framework Core UI features

    main

    The plugin provides a visual interface for several EF Core operations:

    • Creating migrations: Generate new migration files.
    • Updating database: Apply a selected migration to the database (includes migration name autocompletion).
    • DbContext scaffolding: Scaffold a DbContext from an existing database.
    • Creating SQL scripts: Generate SQL scripts from a specified range of migrations.
    • Removing last migration: Remove the most recently created migration.
    • Command Preview: Preview the exact commands that will be executed before running them.
    • Tooling suggestions: Suggests installing dotnet ef command line tools if they are missing from a solution containing EF Core projects.
    • Preference persistence: Remembers common preferences selected in dialogs.
  2. Understand the Data Context abstraction

    main

    In the Rider EF Core plugin, every dialog uses a data context. This is an object that acts as a bridge between the UI layer (containing controls) and the Rider backend (where .NET-related analysis for migrations, DbContexts, etc., resides).

    Data contexts typically use observable properties to allow the UI to react to changes. When implementing a new dialog, you should define a class that extends CommonDataContext to hold the state of your dialog's fields.

    class ScaffoldDbContextDataContext(intellijProject: Project) : CommonDataContext(intellijProject, false) {
        val connection = observable("")
        val provider = observable("")
        val outputFolder = observable("Entities")
        // ... other observable properties
    }
  3. Implement dialog state persistence with `loadState` and `saveState`

    main

    To ensure dialog settings (like selected providers or folder paths) are remembered between sessions, you must implement the loadState and saveState methods within your data context.

    Use DialogsStateService.SpecificDialogState to retrieve and save data.

    Key Requirements:

    1. Call Super: Always call super.loadState() and super.saveState(). The base implementation handles common dialog properties like the Migrations project, Startup project, and Build configuration.
    2. Use Unique Keys: Define a private object with constant keys (e.g., KnownStateKeys) to avoid collisions when saving/loading properties.
    3. Filter Transient Data: Do not save properties intended for single-use (e.g., a specific 'Migration Name' in an 'Add Migration' dialog) as they should not persist across sessions.
    val someProperty = observable("")
    
    override fun loadState(commonDialogState: DialogsStateService.SpecificDialogState) {
        super.loadState()
        
        val somePropertyName = commonDialogState.get(KnownStateKeys.SOME_PROPERTY)
        val somePropertyValue = availableProperties.firstOrNull { it == somePropertyName }
        if (somePropertyValue != null) {
            this.someProperty.value = somePropertyValue
        }
    }
    
    override fun saveState(commonDialogState: DialogsStateService.SpecificDialogState) {
        super.saveState()
    
        val somePropertyValue = someProperty.value
        if (somePropertyValue != null) {
            commonDialogState.set(KnownStateKeys.SOME_PROPERTY, somePropertyValue)
        }
    }
    
    private object KnownStateKeys {
        const val SOME_PROPERTY = "SOME_PROPERTY"
    }
  4. Use Entity Framework Core commands in Rider

    main

    To access the EF Core UI features, follow these steps:

    1. Open a solution containing EF Core related projects (specifically your migrations and startup projects).
    2. Right-click on either your migrations project or your startup project in the project or solution context menu.
    3. Navigate to the <kbd>Entity Framework Core</kbd> menu item.
    4. Select the desired action to open the appropriate dialog.
    5. Click <kbd>Ok</kbd> to execute the command. The output will be displayed in a console-like window.
  5. Develop the EF Core UI plugin

    main

    To develop or build the plugin, you need JDK 25 and .NET SDK 10.0+ installed and configured.

    Build and Run Workflow

    1. Generate RD protocol data (required for internal plugin communication):

      ./gradlew rdgen
    2. Build the plugin (compiles both frontend and backend parts):

      ./gradlew buildPlugin
    3. Run the plugin (starts an instance of Rider with the plugin attached):

      ./gradlew runIde
  6. Execute EF Core commands via the IDE terminal

    main
    The TerminalCommandExecutor is the internal mechanism used to run Entity Framework Core CLI commands within the JetBrains Rider IDE. When a command is executed, it opens a new tab in the EF Core Console tool window, allowing you to view the real-time output (stdout and stderr) of the command. The executor handles ANSI escape sequences to ensure terminal formatting is preserved and captures the exit code and output buffers to determine if the command succeeded or failed.
  7. Use local.settings.json for Azure Functions connections

    main

    The EF Core plugin can automatically discover database connection strings from an Azure Functions local.settings.json file. The provider looks for this file in the parent directory of the project location defined in the RdProjectDescriptor.

    To ensure your connections are discovered, place a valid local.settings.json file in your project directory. The plugin will parse the JSON and extract connection strings using the internal JsonConnectionStringsManager.

  8. Bind and map `ObservableProperty` instances

    main

    You can create reactive relationships between ObservableProperty instances using bind or map.

    One-way Binding

    Use .bind(source) { transform } to create a new property that updates whenever the source changes.

    • Immediate execution: By default, the transformation runs on the next update. To run it immediately for the initial value, set warmUp = true.

    Two-way Binding

    Provide two functions to the .bind method: one to transform the source into the target type, and one to extract the source value from the target type.

    Mapping

    Use .map(toTarget, fromTarget) to create a new property derived from an existing one. This is a more concise way to express the same logic as a two-way binding.

    val firstName = observable("")
    
    // One-way binding with immediate execution (warmUp)
    val greetings = observable("").bind(warmUp = true, firstName) {
        "Hello, {it}!"
    }
    
    // Two-way binding
    data class FirstNameInfo(val firstName: String)
    val firstNameInfo = observable(FirstNameInfo("")).bind(firstName) {
        FirstNameInfo(it) // To target
    } {
        it.firstName    // From target
    }
    
    // Using map (equivalent to two-way binding)
    val firstNameInfoMapped = firstName.map(
        { FirstNameInfo(it) }, // To target
        { it.firstName }       // From target
    )
  9. Use `Event<T>` for multi-listener callbacks

    main

    The Event<T> class acts as a thin helper for managing multiple listeners, similar to a delegate in C#. You can register an arbitrary number of listeners and trigger them all simultaneously using .invoke().

    data class Payload(val data: String)
    
    val onPayloadReceived = Event<Payload>()
    
    // Registering listeners
    onPayloadReceived += { log(it.data) }
    onPayloadReceived += { someCall(it) }
    
    // Triggering all listeners
    onPayloadReceived.invoke(Payload("Hello"))