Dotnet api linux

From bibbleWiki
Jump to navigation Jump to search

Set up with VS Code

Create Project

To create a webapi using .net core 3.1 simply type

dotnet new webapi

Restore Nuget Packages

dotnet add package Microsoft.EntityFrameworkCore

Install Migration Tool

Exercise called for using Add-Migration on windows. In Linux this translates to

# Install Tool
dotnet add package Microsoft.EntityFrameworkCore.Tools.Dotnet
# Install dotnet-ef
dotnet tool install --global dotnet-ef
# Run Migration Creation
dotnet ef migrations add InitialCreate

Install SQL Server

wget -qO- https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -

# Add to /etc/apt/source.list
# deb [arch=amd64,arm64,armhf] https://packages.microsoft.com/ubuntu/18.04/mssql-server-2019 bionic main

sudo apt-get update
sudo apt-get install -y mssql-server
sudo /opt/mssql/bin/mssql-conf setup

# Show working
systemctl status mssql-server --no-pager

Create user

CREATE DATABASE test;
GO

CREATE LOGIN test with PASSWORD = 'guess!';
GO

EXEC master..sp_addsrvrolemember @loginame = N'test', @rolename = N'dbcreator'
GO

Running Query

List tables in DB

select schema_name(t.schema_id) as schema_name,
       t.name as table_name,
       t.create_date,
       t.modify_date
from sys.tables t
order by schema_name,
         table_name;
sqlcmd -S localhost -U test -d CourseLibraryDB -Q list_tables.sql

Structuring and Implementing

Interacting with Resources through HTTP Methods

Below is a table which shows the methods and how they work with the Authors and Courses demo app along with suggested naming. Note the use of nouns

Content Negotiation

We also need to ensure that we configure what type of media we support. In ASP .net this can be done by setting the setupAction. This will stop the default of json being returned.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(setupAction => {
                setupAction.ReturnHttpNotAcceptable = true;
            }).AddXmlDataContractSerializerFormatters();
...

Method Safety and Method Idempotency

Method safety and idempotency help decide which method to use for which use case.

Getting Resources

Intro

When we get resources we generally expect business objects rather than entities. These are known as DTOs (Data Transfer Objects). For C# there are other options but automapper product seems to be the product of choice for ASP .NET.

Adding Automapper

To add automapper

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Configure Automapper

And configure the service in the code

        public void ConfigureServices(IServiceCollection services)
        {
...
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
...

Create a DTO Class

    public class AuthorDto
    {
        public Guid Id { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }

        public string MainCategory { get; set; }
    }

Create a Profile Class

This class performs the mapping from the Entity to the DTO

public class AuthorsProfile : Profile
{
    public AuthorsProfile()
    {
        CreateMap<Entities.Author, Models.AuthorDto>()
            .ForMember(
               dest => dest.Name,
               opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
            .ForMember(
               dest => dest.Age,
               opt => opt.MapFrom(src => src.DateOfBirth.GetCurrentAge()));
        }
    }
}

Implement the Get of collection and instance on the controller

...
        [HttpGet()]
        public ActionResult<IEnumerable<AuthorDto>> GetAuthors()
        {
            var authorsFromRepo = _courseLibraryRepository.GetAuthors();
            return Ok(_mapper.Map<IEnumerable<AuthorDto>>(authorsFromRepo));
        }
        [HttpGet("{authorId}")]
        public IActionResult GetAuthor(Guid authorId)
        {
            var authorsFromRepo = _courseLibraryRepository.GetAuthor(authorId);
            if (authorsFromRepo == null)
            {
                return NotFound();
            }
            return Ok(_mapper.Map<AuthorDto>(authorsFromRepo));
        }

Overriding Exception Errors

To control the error the consumer sees in the case of an exception, configure an exception handler in Startup.cs

...
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler(appBuilder => {
                    appBuilder.Run(async context => {
                        context.Response.StatusCode = 500;
                        await context.Response.WriteAsync("An unexpected fault occurred. Please try again");
                    });
                });
            }

Supporting HEAD

To support just sending the result without the body simply add HttpHead to the getters.

        [HttpGet()]
        [HttpHead]
        public ActionResult<IEnumerable<AuthorDto>> GetAuthors()
        {
            var authorsFromRepo = _courseLibraryRepository.GetAuthors();
            return Ok(_mapper.Map<IEnumerable<AuthorDto>>(authorsFromRepo));
        }

Filtering And Searching

Bindings

To get the arguments for filtering and search ASP .NET uses the following

  • [FromQuery] - Gets values from the query string.
  • [FromRoute] - Gets values from route data.
  • [FromForm] - Gets values from posted form fields.
  • [FromBody] - Gets values from the request body.
  • [FromHeader] - Gets values from HTTP headers.

Filtering

Basically add an argument to the get method and implement it on the data repository checking for an argument and returning appropriately. Note the default [FromQuery] argument is the key to this being passed. E.g. mainCategory=Rum on the URI

# In the controller
        [HttpGet()]
        [HttpHead]
        public ActionResult<IEnumerable<AuthorDto>> GetAuthors(string mainCategory)
        {
            var authorsFromRepo = _courseLibraryRepository.GetAuthors(mainCategory);
            return Ok(_mapper.Map<IEnumerable<AuthorDto>>(authorsFromRepo));
        }

# In the Repository
        public IEnumerable<Author> GetAuthors(string mainCategory)
        {
            if (string.IsNullOrEmpty(mainCategory)) return _context.Authors.ToList<Author>();
            return _context.Authors.Where(x => x.MainCategory == mainCategory.Trim()).ToList();
        }

Searching

To implement this just add a parameter for the searching as you did for filtering and implement appropriately

        public IEnumerable<Author> GetAuthors(string mainCategory, string searchQuery)
        {
            if (string.IsNullOrEmpty(mainCategory) && string.IsNullOrEmpty(searchQuery)) return _context.Authors.ToList<Author>();

            var collection = _context.Authors as IQueryable<Author>;

            if (!string.IsNullOrEmpty(mainCategory))
            {
                collection = collection.Where(x => x.MainCategory == mainCategory.Trim());
            }

            if (!string.IsNullOrEmpty(searchQuery))
            {
                collection = collection.Where(
                    x => x.MainCategory.Contains(searchQuery.Trim()) ||
                         x.FirstName.Contains(searchQuery.Trim()) ||
                         x.LastName.Contains(searchQuery.Trim()));
            }

            return collection.ToList();
        }

Combining Parameters

Make a class to contain these and pass them to the repository.

    public class AuthorsResourceParameters
    {
        public string MainCategory { get; set; }
        public string SearchQuery { get; set; }

    }

You will need to tell the controller where to find them as it is not the default. i.e.

        [HttpGet()]
        [HttpHead]
        public ActionResult<IEnumerable<AuthorDto>> GetAuthors([FromQuery] AuthorsResourceParameters authorsResourceParameters)
        {
            var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorsResourceParameters);
            return Ok(_mapper.Map<IEnumerable<AuthorDto>>(authorsFromRepo));
        }

Creating Resources

Create a DTO for creations

    public class AuthorForCreationDto
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTimeOffset DateOfBirth { get; set; }
        public string MainCategory { get; set; }
    }

Create a profile

This converts the DTO to an entity

    public class AuthorsProfile : Profile
    {
       CreateMap<Models.AuthorForCreationDto, API.Entities.Author>();
    }

Implement Post

Add a name to your GET for your entity

        [HttpGet("{authorId}", Name="GetAuthor")]
        public IActionResult GetAuthor(Guid authorId)
        {
...
        }

Most of the code is simple except for the CreateAtRoute. This is used to add a Location header to the response. The Location header specifies the URI of the newly created item.

        [HttpPost]
        public ActionResult<AuthorDto> CreateAuthor(AuthorForCreationDto author)
        {
            var authorEntity = _mapper.Map<Author>(author);
            _courseLibraryRepository.AddAuthor(authorEntity);
            _courseLibraryRepository.Save();

            var authorToReturn = _mapper.Map<AuthorDto>(authorEntity);

            return CreatedAtRoute("GetAuthor",        // the name of the route
                new { authorId = authorToReturn.Id }, // the parameters of the route
                authorToReturn);                      // the DTO to create the DTO from
        }

Creating Resources For a Collection

This is the same as above with the exception of the Location header. If you are creating a collection then the location header will need to support the retrieval of the collection post creation.

Creating the Post Collection Request

This is identical to the single request but using an array.

[HttpPost]
        public ActionResult<IEnumerable<AuthorDto>> CreateAuthorCollections(IEnumerable<AuthorForCreationDto> authorCollection)
        {
            var authorEntities = _mapper.Map<IEnumerable<Author>>(authorCollection);
            foreach (var author in authorEntities)
            {
                _courseLibraryRepository.AddAuthor(author);
            }
            _courseLibraryRepository.Save();

            var authorCollectionToReturn = _mapper.Map<IEnumerable<AuthorDto>>(authorEntities);

            return CreatedAtRoute("GetAuthorCollection",
                new { authorId = string.Join(",", authorCollectionToReturn.Select(x => x.Id)) },
                authorCollectionToReturn);
        }

Creating the Get Collection Request

Everything is the same except you need a [FromRoute] and a ModelBinder. The model binder needs to create and array of composite keys so that all of the items can be retrieved.

        [HttpGet("({authorIds})", Name = "GetAuthorCollection")]
        public IActionResult GetAuthorCollection(
            [FromRoute]
            [ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<Guid> authorIds)
        {
            if (authorIds == null)
            {
                return BadRequest();
            }

            var authorsFromRepo = _courseLibraryRepository.GetAuthors(authorIds);

            if (authorIds.Count() != authorsFromRepo.Count())
            {
                return NotFound();
            }

            return Ok(_mapper.Map<IEnumerable<AuthorDto>>(authorsFromRepo));
        }

Creating Array Model Binder

What is Model Binding

What is Model binding Controllers and Razor pages work with data that comes from HTTP requests. For example, route data may provide a record key, and posted form fields may provide values for the properties of the model. Writing code to retrieve each of these values and convert them from strings to .NET types would be tedious and error-prone. Model binding automates this process. The model binding system:

Retrieves data from various sources such as route data, form fields, and query strings. Provides the data to controllers and Razor pages in method parameters and public properties. Converts string data to .NET types. Updates properties of complex types.

My Example of Model Binding

Tried to provide explanation in the comments but basically if checks if sane and then returns an appropriate array of composite keys.

    public class ArrayModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            // Our binder only works on enumerable types
            if(!bindingContext.ModelMetadata.IsEnumerableType)
            {
                bindingContext.Result = ModelBindingResult.Failed();
                return Task.CompletedTask;
            }

            // Get Inputted value from the value provided
            var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();

            // If the value is null or whitespace, we return null
            if(string.IsNullOrWhiteSpace(value))
            {
                bindingContext.Result = ModelBindingResult.Success(null);
                return Task.CompletedTask;
            }
 
            // If the model is enumerable and 
            //    the value isn't null or whitespace
            //       Get the enumerable types and a converter
            var elementType = bindingContext.ModelMetadata.ModelType.GetTypeInfo().GetGenericArguments()[0];


            var elementTypeConvertor =  TypeDescriptor.GetConverter(elementType);

            // Convert each item in the value list to the enumerable type
            var valuesAsStringArray = value.Split( new[] {","}, StringSplitOptions.RemoveEmptyEntries);
            var values = valuesAsStringArray.Select(x=> elementTypeConvertor.ConvertFrom(x.Trim())).ToArray();
            
            // Create an array of the same length received and the element type in the binding context
            var typedValues = Array.CreateInstance(elementType, values.Length);
            // Copy the string values to to the typed values
            values.CopyTo(typedValues,0);
            // Set the model
            bindingContext.Model = typedValues;

            // Return the result
            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
            return Task.CompletedTask;
            
        }
    }