Part 2: Build containerized microservices with Docker and .NET Core 2.1

June 15, 2018 by rdagumampan

This is part of a five-part series in my exploration of Azure microservices facilities.

  1. Kick-start containerized microservice CI/CD with Azure DevOps
  2. Build containerized microservices with Docker and .NET Core 2.1
  3. Build containerized web dashboard with ASP.NET Core
  4. Email service health report with Azure serverless
  5. Scale out services with Managed Kubernetes

Building the REST API

There are two ways to create our REST API and each have pros and cons:
1. Create a new Azure DevOps project like we did in part 1.
2. Pull from existing ASP.NET core examples from GitHub. https://github.com/dotnet/dotnet-docker.

I have chosen to jumpstart with Azure DevOps project so I don’t have to do lot of scafolding and CI/CD. And because the default project created is an ASP.NET MVC web app, we need to do some clean-up.

  1. Cleanup project file. Changed the target to .NET Core 2.1 and remove all unnecessary dependencies.

    <Project Sdk="Microsoft.NET.Sdk.Web">
        <PropertyGroup>
            <TargetFramework>netcoreapp2.1</TargetFramework>
        </PropertyGroup>
        <ItemGroup>
            <PackageReference Include="Microsoft.AspNetCore.App"/>
        </ItemGroup>
        <ItemGroup>
            <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
        </ItemGroup>
    </Project>
    
  2. Cleanup docker file. Changed the target to .NET Core 2.1 and publish before we build the docker image

    FROM microsoft/dotnet:2.1-sdk AS build
    WORKDIR /app
    
    # Copy csproj and restore as distinct layers
    COPY *.csproj ./
    RUN dotnet restore
    
    # Copy everything else and build
    COPY . ./
    RUN dotnet publish -c Release -o out
    
    # Build runtime imag.e
    FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime
    WORKDIR /app
    COPY --from=build /app/out .
    ENTRYPOINT ["dotnet", "aspnet-core-dotnet-core.dll"]
    
  3. Refactor startup. We remove web app specific activities.

    //app.UseBrowserLink();
    //app.UseStaticFiles();
    ...
    app.UseMvc();
    
  4. Create API controller

    [Route("api/[controller]")]
        [ApiController]    
        public class ServiceHealthController : Controller
        {
            private readonly IServiceHealthRepository _serviceHealthRepository = new ServiceHealthRepository();
            private readonly IServiceHealthHistoryRepository _serviceHealthHistoryRepository = new ServiceHealthHistoryRepository();
    
            [HttpGet]
            public ActionResult<List<ServiceHealthDto>> Get()
            {
                var result = _serviceHealthRepository.All().ToList();
                var statuses = result.Select(i=> new ServiceHealthDto
                {
                    ServiceId = i.ServiceId,
                    ServiceName = i.ServiceName,
                    Description = i.Description,
                    Location = i.Location,
                    LastPing = i.LastPing,
                    LastStatus = i.LastStatus,
                    AliveSince = (DateTime.UtcNow - i.LastPing).TotalSeconds
                }).ToList();
    
                return statuses;
            }
    
            [HttpPost] 
            public void Post([FromBody]ServiceHealthDto status)
            {
                var result = SaveStatus(status);
                SaveHistory(status);
            }
    
            private int SaveHistory(ServiceHealthDto status)
            {
                var service = new ServiceHealthDbModel
                {
                    ServiceId = status.ServiceId,
                    ServiceName = status.ServiceName,
                    Description = status.Description,
                    Location = status.Location,
                    LastPing = DateTime.UtcNow,
                    LastStatus = status.LastStatus
                };
    
                var rowsAffected = _serviceHealthHistoryRepository.Create(service);
                return rowsAffected;
            }
    
            private int SaveStatus(ServiceHealthDto status)
            {
                var service = _serviceHealthRepository
                            .All()
                            .FirstOrDefault(f => f.ServiceId == status.ServiceId);
    
                if (null != service)
                {
                    service.LastPing = DateTime.UtcNow;
                    service.LastStatus = status.LastStatus;
                    _serviceHealthRepository.Update(service);
                }
                else
                {
                    service = new ServiceHealthDbModel
                    {
                        ServiceId = status.ServiceId,
                        ServiceName = status.ServiceName,
                        Description = status.Description,
                        Location = status.Location,
                        LastPing = DateTime.UtcNow,
                        LastStatus = status.LastStatus
                    };
                    _serviceHealthRepository.Create(service);
                }
                return 1;       
            }   
        } 
    
  5. Create an Azure SQL database and execute these

    CREATE TABLE [dbo].[ServiceHealth](
        [ServiceId] [nvarchar](50) NOT NULL,
        [ServiceName] [nvarchar](255) NOT NULL,
        [Description] [nvarchar](255) NOT NULL,
        [Location] [nvarchar](50) NOT NULL,
        [LastPing] [datetime] NOT NULL,
        [LastStatus] [nvarchar](255) NOT NULL,
    CONSTRAINT [PK_ServiceHealth] PRIMARY KEY CLUSTERED 
    (
        [ServiceId] ASC
    )WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
    ) ON [PRIMARY];
    GO
    
    ALTER TABLE [dbo].[ServiceHealth] ADD  CONSTRAINT [DF_ServiceHealth_Location]  DEFAULT ('Waiting for heartbeat') FOR [Location];
    GO
    ALTER TABLE [dbo].[ServiceHealth] ADD  CONSTRAINT [DF_ServiceHealth_LastPing]  DEFAULT (getdate()) FOR [LastPing];
    GO
    ALTER TABLE [dbo].[ServiceHealth] ADD  CONSTRAINT [DF_ServiceHealth_LastStatus]  DEFAULT ('Waiting for heartbeat') FOR [LastStatus];
    GO
    
    CREATE TABLE [dbo].[ServiceHealthHistory](
        [ServiceId] [nvarchar](50) NOT NULL,
        [ServiceName] [nvarchar](255) NOT NULL,
        [Description] [nvarchar](255) NOT NULL,
        [Location] [nvarchar](50) NOT NULL,
        [TimestampUtc] [datetime] NOT NULL
    ) ON [PRIMARY];
    GO
    
    ALTER TABLE [dbo].[ServiceHealthHistory] ADD  CONSTRAINT [DF_ServiceHealth_TimestampUtc]  DEFAULT (getutcdate()) FOR [TimestampUtc];
    GO
    
  6. Create repositories

    public class ServiceHealthRepository : IServiceHealthRepository
    {
        string connectionString = "...";
    
        public List<ServiceHealthDbModel> All()
        {
            var results = new List<ServiceHealthDbModel>();
    
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                var command = connection.CreateCommand();
                command.CommandType = System.Data.CommandType.Text;
                command.CommandText = "SELECT * FROM [dbo].[ServiceHealth];";
    
                var reader = command.ExecuteReader();
                while (reader.Read())
                {
                    var status = new ServiceHealthDbModel
                    {
                        ServiceId = reader.GetString(0),
                        ServiceName = reader.GetString(1),
                        Description = reader.GetString(2),
                        Location = reader.GetString(3),
                        LastPing = reader.GetDateTime(4),
                        LastStatus = reader.GetString(5),
                    };
                    results.Add(status);
                }
    
                return results;
            }
        }
    
        public int Create(ServiceHealthDbModel status)
        {
            using(var connection = new SqlConnection(connectionString))
            {
                connection.Open();
    
                var command = connection.CreateCommand();
                command.CommandType = System.Data.CommandType.Text;
                command.CommandText = @"
                INSERT INTO [dbo].[ServiceHealth]
                       ([ServiceId]
                       ,[ServiceName]
                       ,[Description]
                       ,[Location]
                       ,[LastPing]
                       ,[LastStatus]
                ) VALUES (
                        @ServiceId
                       ,@ServiceName
                       ,@Description
                       ,@Location
                       ,@LastPing
                       ,@LastStatus
                    );";
    
                command.Parameters.AddWithValue("@ServiceId", status.ServiceId);
                command.Parameters.AddWithValue("@ServiceName", status.ServiceName);
                command.Parameters.AddWithValue("@Description", status.Description);
                command.Parameters.AddWithValue("@Location", status.Location);
                command.Parameters.AddWithValue("@LastPing", status.LastPing);
                command.Parameters.AddWithValue("@LastStatus", status.LastStatus);
    
                var affectedRows = command.ExecuteNonQuery();
                return affectedRows;
            }
        }
    
        public int Update(ServiceHealthDbModel status)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
    
                var command = connection.CreateCommand();
                command.CommandType = System.Data.CommandType.Text;
                command.CommandText = @"
                UPDATE [dbo].[ServiceHealth]
                SET
                        [Location] = @Location
                       ,[LastPing] = @LastPing
                       ,[LastStatus] = @LastStatus
                WHERE
                    [ServiceId] = @ServiceId;
                ";
    
                command.Parameters.AddWithValue("@ServiceId", status.ServiceId);
                command.Parameters.AddWithValue("@Location", status.Location);
                command.Parameters.AddWithValue("@LastPing", status.LastPing);
                command.Parameters.AddWithValue("@LastStatus", status.LastStatus);
    
                var affectedRows = command.ExecuteNonQuery();
                return affectedRows;
            }
        }
    }
    
    public class ServiceHealthHistoryRepository : IServiceHealthHistoryRepository
    {
        string connectionString = "...";
        public int Create(ServiceHealthDbModel status)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
    
                var command = connection.CreateCommand();
                command.CommandType = System.Data.CommandType.Text;
                command.CommandText = @"
                INSERT INTO [dbo].[ServiceHealthHistory]
                       (
                        [TimestampUtc]
                       ,[ServiceId]
                       ,[ServiceName]
                       ,[Description]
                       ,[Location]
                ) VALUES (
                        @LastPing
                       ,@ServiceId
                       ,@ServiceName
                       ,@Description
                       ,@Location
                    );";
    
                command.Parameters.AddWithValue("@LastPing", status.LastPing);
                command.Parameters.AddWithValue("@ServiceId", status.ServiceId);
                command.Parameters.AddWithValue("@ServiceName", status.ServiceName);
                command.Parameters.AddWithValue("@Description", status.Description);
                command.Parameters.AddWithValue("@Location", status.Location);
    
                var affectedRows = command.ExecuteNonQuery();
                return affectedRows;
            }
        }
    }
    
  7. Dockerize it

/ docker build . -t servicehealth-api
/ docker images
  1. Test it
/ docker run -d -p 8081:80 --name servicehealth-api servicehealth-api
/ docker ps
/ curl http://localhost:8081/api/servicehealth/
/ curl http://localhost:8081/api/servicehealth/

© 2017 | About | Contact | Follow me on Twitter | Powerered by Hucore & Hugo