Skip to content
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

Add basic Entity Framework connection to the MSSQL Database #4

Merged
merged 5 commits into from Jul 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 48 additions & 6 deletions README.md
@@ -1,12 +1,12 @@
# Net Core With Docker Continuous Integration

This example shows how to:
- setup work environment for .NET Core,
- create simple WebApi project with database usage,
- setup databases without needed to install anything more than docker,
- setup Continuous Integration/Delivery pipeline to make sure that your code runns properly,
- create test environment,
- create prod environment.
- [x] setup work environment for .NET Core,
- [x] create simple WebApi project with database usage,
- [ ] setup databases without needed to install anything more than docker,
- [ ] setup Continuous Integration/Delivery pipeline to make sure that your code runns properly,
- [ ] create test environment,
- [ ] create prod environment.

## Setup work environment for .NET Core
1. Install [Docker](https://www.docker.com/get-docker)
Expand All @@ -23,3 +23,45 @@ We will use default Visual Studio template for the WebApi project:
7. If everything went properly then you should see browser page with `http://localhost:{port}/api/values` and `["value1","value2"]`.

You can check the detailed changes in [pull request](https://github.com/oskardudycz/NetCoreWithDockerCI/pull/2/files)

## Add MSSQL Database to the Web Api project
Most of our applications needs to have the database. We'll use Entity Framework and MSSQL server in this example.
1. From the `Package Manger Console` run `Install-Package Microsoft.EntityFrameworkCore.SqlServer` and `Install-Package Microsoft.EntityFrameworkCore.Tools`. This will add Nuget Packages nessesary for the MSSQL server databasse usage.
2. Create Entity Class (eg. [Task](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/c3b2dc31fb7ae8b834b94cb338b49fd3a8dbe2b5/src/NetCoreWithDocker/NetCoreWithDocker/Storage/Entities/Task.cs)) and DbContext (eg. [TasksDbContext](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Storage/TasksDbContext.cs)).
3. You should get simmilar changes as in this [commit](https://github.com/oskardudycz/NetCoreWithDockerCI/pull/4/commits/c3b2dc31fb7ae8b834b94cb338b49fd3a8dbe2b5).
4. Next step is to provide the connection string to the database. For this example we'll use LocalDB, which is distributed and installed automatically with the Visual Studio 2017 (if you're not using the Visual Studio then you can get it from this [link](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb)).
5. We need to provide proper connection string to [appsettings.json](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/appsettings.Development.json) and pass it to the Entity Framework configuration in [Startup.cs](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Startup.cs).
6. Having that configuration we can remove the dummy `ValuesController` and add new [TasksController](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Controllers/TasksController.cs).
This example contains few important things derived from the best practices like:
* `async` usage - it' much better for the performance perspective to use `async/await` in the api. What's crucial is that you cannot leave the async statement using database not awaited, because it may end up with your db connection not disposed properly,
* returning proper Http Status Codes for the Api endpoints:
* `OK` - will return `200`,
* `NotFound`- will return `404` result,
* `Forbid` - will return `403` status,

Good explanation of http status codes can be found [here](https://ict.ken.be/Data/Sites/1/images/articles/http-status-code-explained.jpg)
* few examples of the new usefull [C# 6 syntax](https://msdn.microsoft.com/en-us/magazine/dn802602.aspx)
7. You should also update your `launchSettings.json` to redirect you by default to the `/tasks/` instead of the `/values/` route.
8. If you run now you're application then you'll get following exception:

``
System.Data.SqlClient.SqlException: 'Cannot open database "NetCoreWithDocker" requested by the login. The login failed.
``

It basically means that it cannot login, because in fact there is no database that Entity Framework can login to. How to setup it? By using build in Migrations mechanism.
9. It's needed to setup the migrations (event the initial one), that will create database with object<=>relation mapping defined in the [TasksDbContext](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Storage/TasksDbContext.cs). To do that open `Package Manager Console` and run: `Add-Migration InitialCreate](). This will automatically freeze the current model definiton in [Current Model Snapshot](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/Migrations/TasksDbContextModelSnapshot.cs) and the define [Initial Migration](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/Migrations/20170730135615_InitialCreate.cs).
10. Now to create/update our database it's needed to run `Update-Database` from `Package Manager Console`.
11. You should now be able to run the application. If you did all of the steps properly then you should see browser page with `http://localhost:{port}/api/values` and `[]`. That means that our [TasksController](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Controllers/TasksController.cs) returned empty list of tasks - because our database is currently empty.
12. Entity Framework also provides mechanism to add initial data (it's usefull for the eg. dictionaries or test data setup). Unfortunatelly it's needed to provide some boilerplate code for that. We need to do following steps:

12.1. Run `Add-Migration InitialSeed` in the `Package Manager Console` - this will generate empty migration.

12.2. Then we need to prepare our [TasksDbContext](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/df641d876b094eb64918c3823ede6a14529216e4/src/NetCoreWithDocker/NetCoreWithDocker/Storage/TasksDbContext.cs) to be also created not only from the depenedency injection. To do that we should create [TasksDbContextFactory](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/Storage/TasksDbContextFactory.cs). This class will be used for the database configuration (eg. connection string) injection. It needs to implement `IDbContextFactory<TasksDbContext>` and unfortunatelly mimic some of the Startup Configuration reading functionality. Full implementation can be seen [here](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/Storage/TasksDbContextFactory.cs).

12.3. Now we can use our factory class in our [Initial Seed](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/Migrations/20170730140332_InitialSeed.cs) migration. In `Up` method we're defining our insertion, in `Down` method we're cleaning data (it' being run if something went wrong).

12.4. You need also to mark `appsettings.Development.json` as being coppied to ouput directory. To do that we need to add new settings in the [NetCoreWithDocker.csproj](https://github.com/oskardudycz/NetCoreWithDockerCI/blob/da1ab7345306933aafcce0002ee4ba54cd437d8b/src/NetCoreWithDocker/NetCoreWithDocker/NetCoreWithDocker.csproj).

Now you should be able to run `Update-Database` from `Package Manager Console` to apply seeding, start application by clicking `F5` and see the results in the browser (eg. `[{"id":1,"description":"Do what you have to do"},{"id":2,"description":"Really urgent task"},{"id":3,"description":"This one has really low priority"}]`)

You can check the detailed changes in [pull request](https://github.com/oskardudycz/NetCoreWithDockerCI/pull/4/files)
@@ -0,0 +1,82 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NetCoreWithDocker.Storage;
using NetCoreWithDocker.Storage.Entities;
using Microsoft.EntityFrameworkCore;
using Threading = System.Threading.Tasks;

namespace NetCoreWithDocker.Controllers
{
[Route("api/[controller]")]
public class TasksController : Controller
{
private readonly TasksDbContext dbContext;

private DbSet<Task> Tasks => dbContext?.Tasks;

public TasksController(TasksDbContext dbContext)
{
this.dbContext = dbContext ?? throw new ArgumentException(nameof(dbContext));
}

// GET api/tasks
[HttpGet]
public async Threading.Task<IActionResult> Get()
{
return Ok(await Tasks.ToListAsync());
}

// GET api/tasks/5
[HttpGet("{id}")]
public async Threading.Task<IActionResult> Get(int id)
{
var result = await Tasks.FindAsync(id);

if(result == null)
return NotFound(id);

return Ok(result);
}

// POST api/tasks
[HttpPost]
public async Threading.Task<IActionResult> Post([FromBody]Task task)
{
if (await Tasks.AllAsync(t => t.Id != task.Id))
return NotFound(task.Id);

Tasks.Update(task);
await dbContext.SaveChangesAsync();

return Ok();
}

// PUT api/tasks/5
[HttpPut("{id}")]
public async Threading.Task<IActionResult> Put(int id, [FromBody]Task task)
{
if (Tasks.Any(t => t.Id == task.Id))
return Forbid();

Tasks.Add(task);
await dbContext.SaveChangesAsync();

return Ok();
}

// DELETE api/tasks/5
[HttpDelete("{id}")]
public async Threading.Task<IActionResult> Delete(int id)
{
var result = await Tasks.FindAsync(id);

if (result == null)
return NotFound(id);

Tasks.Remove(result);

return Ok();
}
}
}

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace NetCoreWithDocker.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Tasks",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Description = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Tasks", x => x.Id);
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tasks");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NetCoreWithDocker.Storage;
using NetCoreWithDocker.Storage.Entities;

namespace NetCoreWithDocker.Migrations
{
public partial class InitialSeed : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
using (var db = new TasksDbContextFactory().Create(null))
{
db.Tasks.AddRange(
new Task { Description = "Do what you have to do" },
new Task { Description = "Really urgent task" },
new Task { Description = "This one has really low priority" });
db.SaveChanges();
}
}

protected override void Down(MigrationBuilder migrationBuilder)
{
using (var db = new TasksDbContextFactory().Create(null))
{
db.Tasks.RemoveRange(db.Tasks);
db.SaveChanges();
}
}
}
}
@@ -0,0 +1,32 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using NetCoreWithDocker.Storage;

namespace NetCoreWithDocker.Migrations
{
[DbContext(typeof(TasksDbContext))]
partial class TasksDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
modelBuilder
.HasAnnotation("ProductVersion", "1.1.2")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

modelBuilder.Entity("NetCoreWithDocker.Storage.Entities.Task", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();

b.Property<string>("Description");

b.HasKey("Id");

b.ToTable("Tasks");
});
}
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
Expand All @@ -11,10 +11,17 @@
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>