Skip to content
Sergey Tregub edited this page Jan 17, 2019 · 17 revisions

Welcome to the ASP.Net Core RESTful Service Template WIKI!

Cross-Origin Resource Sharing (CORS)

Quote from an MDN article:

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application makes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.

By default, all origins, methods and headers are allowed. If you want more control, then you can adjust CORS-policy in the Startup.cs file.

You can find more information in the official documentation.

Options Verb handler

TBD

Deploying to IIS

web.config adjust max query string length, max request size, max upload file size and so on

TBD

Dependency Injection

Dependency Injection is a form of inversion of control that supports the dependency inversion design principle.
This project uses Autofac as an IoC Container, but it can be replaced with any other if you will.

First of all, you must configure a container. There are two methods in the Startup.cs to do this - ConfigureContainer and ConfigureProductionContainer. The latest only gets called if your environment is Production. Read comments in these methods to get more information.

You can place your registration there directly, but I don't recommend that. According to the official documentation, a recommended way is to use so-called modules. The project already has a default module (Configuration\AutofacModules\DefaultModule.cs) that you can use. Just register your components and services there and have fun. For example:

builder.RegisterType<Repo.ProductsRepo>().As<Repo.IProductsRepo>().SingleInstance();

If you add a new module, please, don't remember to add it to a container in one of the methods - ConfigureContainer or ConfigureProductionContainer:

builder.RegisterModule<DefaultModule>();

Second, you typically use registered types and interfaces in constructors of your controllers or of other services.

IProductsRepo ProductsRepo { get; }

public ProductsController(IProductsRepo productsRepo)
{
    ProductsRepo = productsRepo ?? throw new ArgumentNullException(nameof(productsRepo));
}

productsRepo parameter will be autowired by the container.

You can read more on this in the official Autofac documentation.

AutoMapper

When you create a web-service, it is a typical case to map one object to another. For example, you may want to map entity read from DB to DTO (Data Transmission Object) to return it as a response. You can write custom code to do that mapping or use a ready-made solution. AutoMapper is a solution to this problem.

Quote from the official site:

AutoMapper is a simple little library built to solve a deceptively complex problem - getting rid of code that mapped one object to another

The detailed information about using it in different cases you can get from official documentation.

As an additional advantage of using Automapper, you get segregation of mapping code from using it. Here we focus on the most common cases and some simple examples to give you a starting point.

The project already contains a ready to use configuration for AutoMapper. It is integrated with Autofac so you can inject IMapper interface to your services or controllers, or you can use Autofac to resolve services in mapping code! All the mapping code gathered in so-called profiles. The profile is a class inherited from Automapper.Profile. The project already has one for demo purposes (see Configuration\AutoMapperProfiles\DefaultProfile.cs). You can use that one or add as many profiles as you want. To add a profile, you must create a new class derived from AutoMapper.Profile. At startup time your profile discovered and loaded automatically. If you want to use some service registered in DI-container, you ought to inject its interface in a profile constructor.

Let's say for example you need to return Dto.Product object from some method of your controller, but you have Model.Product in your code. First of all, we should create a mapping from Model.Product to Dto.Product. This can be done in Configuration\AutoMapperProfiles\DefaultProfile.cs:

  CreateMap<Model.Product, Dto.Product>();

Second, use it in your controller:

  [HttpGet]
  public IEnumerable<Dto.Product> Get()
  {
      return ProductsRepo.Get().Select(Mapper.Map<Dto.Product>);
  }

Let's see a couple of examples of how to configure mappings:

cfg.CreateMap<Model.Product, Dto.Product>()
  // Using a custom constructor
  .ConstructUsing(src => new Dto.Product(src.Id))
  // Using a custom formatting
  .ForMember(x => x.Date, o => o.ResolveUsing((src, dest, destMember, context) =>
    $"{src.Year}-{src.Month:D2}-{src.Day:D2}"))
  // Calculated value from another fields of original object
  .ForMember(x => x.Contract, o => o.ResolveUsing((src, dest, destMember, context) =>
    $"{src.Contract.Number} from {src.Contract.Date}"))
  .AfterMap((src, dest, ctx) =>
  {
    // Resolve service from DI-container
    dest.SupplierName = c.Options.CreateInstance<ISuppliersRepo>().GetById(src.SupplierId);
  });

Logging

Each server in a production environment must log information messages and errors. There are many libraries to do this. This project template use Serilog. You don't need to do any configuration. All things are configured for you and work without any additional configuration.

To use a logger, you must inject ILogger<YourClass> interface into any controller or service, as usual you do in any ASP.Net Core application. Here's an example:

public class ProductsContoller
{
  ILogger Logger { get; }

  public ProductsContoller(ILogger<ProductsContoller> logger)
  {
    Logger = logger ?? throw new ArgumentNullException(nameof(logger));
  }

  public IActionResult Create(int id, [FromBody]Dto.UpdateProduct newProductDto)
  {
    // ... other code
    Logger.LogInformation("New product was created: {@product}", createdProduct);
    // some more code
  }
}

You can use Error, Warning and so on instead of Information in the example above. By default, Debug is used as a minimum log level for a development environment and Warning for a production one, but you can alter it in the appsettings.json file.

Log files are created in %AppData%/Logs folder, specific for the current process' user. For example, if your server is running under the AppService user account, then the folder will be C:\Users\AppService\AppData\Roaming\Logs. A name of the file is the same as a service main assembly name with '.txt' extension. Log files are rotated each day, and the folder stores log files for the last 30 days.

If you want you can change log file folder in settings as well as any other Serilog options. More information about using and configuring Serilog you can get in the official documentation.

Cache control

In most cases, you don't want to allow a browser to cache server responses. The project use Filters/CacheControlFilter.cs to add cache-control header to all responses for any of GET requests. It is enough for the most cases, but you can change this as needed.

Unhandled exceptions handling

If something goes wrong and a server has crashed, we want to know what is happening exactly and where it is. It is very useful to log all of the unhandled exceptions.

By default, ASP.Net Core returns 500 Internal Server error HTTP status with the exception message and call stack in case of any exceptions. However, this is not a recommended way to report errors for REST-services. Typically we want to return 401 status code if requested object not found, 403 if the action is not permitted and so on.

The project contains Middleware/ExceptionMiddleware.cs to do this. The idea is simple. We check whether an exception is one of the well-known types and if so, create and return an appropriate HTTP status code and a response and also log additional information about the exception.

Let's see an example:

async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
    int statusCode = 500;

    context.Response.ContentType = "application/json";
    context.Response.StatusCode = statusCode;

    // We can decide what the status code should return
    if (ex is KeyNotFoundException)
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
    }
    else if (ex is DuplicateKeyException)
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
    }

    await context.Response.WriteAsync(
        JsonConvert.SerializeObject(
            new ErrorResponse(ex, Environment.IsDevelopment())));

    if (context.Response.StatusCode == StatusCodes.Status500InternalServerError)
    {
        Logger.LogError(ex, "Unhandled exception occurred");
    }
    else
    {
        Logger.LogDebug(ex, "Unhandled exception occurred");
    }
}

You should define your own exceptions and handle them here to get the right HTTP status code. For more information about exceptions see the official documentation.

Content formatting

Formatters are used by ASP.Net Core to deserialize data from a request and serialize a response. Generally, we want to use JSON for that. The project provides the default options, but you can adjust them as you want in the Startup.cs file.

Using environment variables in configuration options

"Build once - deploy anywhere" is a useful principle that gives us the ability to deploy an app to any environment with ease. Environment variables are a recommended way to store any config options that can vary from one environment to another. If you want to know more about this, you can learn 12 Factor Apps Methodology.

The standard implementation of IConfiguration does not expand environment variables, which you can use in configuration options, so we need a wrapper to do this manually. A file Settings.cs contains helper methods and example settings that can use environment variables.

Let's see an example.

First, let's add some config option to appsettings.json and use env var in it:

"CoolServiceEndpoint": "http://%ENDPOINT_HOST%/cool"

Second, we should create a new property in class Settings.

public static class Settings
{
    public static string GetConnectionString(string name) => Environment.ExpandEnvironmentVariables(Startup.Configuration.GetConnectionString(name));
    public static string Get(string name) => Environment.ExpandEnvironmentVariables(Startup.Configuration[name]);

    public static class Services
    {
        public static string CoolService { get; } = Get("CoolServiceEndpoint");
    }
}

Ok, now we can set up any options in environment variables but is there "the right" way to set the env vars up? Of cause, we can set them up on OS-level, but it is not the only way to do this. In the Unix-world, the dot-env files (.env) are a convenient way to set up environment variables. Luckily, we can use DotNetEnv NuGet-package to do the same thing on Windows.

All that we need is to create a file with .env name and put it near the appsettings.json of our app. The Startup.cs file already contains all configurations for that.

So, let's create this file and add our variable from the example above:

ENDPOINT_HOST=localhost

After that the Settings.Services.CoolService property returns http://localhost/cool value.

Documenting API

Documentation is a crucial thing for those who use your API. However, creating the documentation is a somewhat boring process. Moreover, once you wrote the documentation, you ought to publish it somewhere.

Swagger is a full and comprehensive solution to that problem. Documenting an API is only one of the features, the full list of them you can find on the official site.

This project uses Swashbuckle. "Swagger tooling for API's built with ASP.NET Core. Generate beautiful API documentation, including a UI to explore and test operations, directly from your routes, controllers and models" - as written in it's GitHub repo. This library does automatically create interactive documentation for all controllers' actions and models as an HTML-page which is hosted within your service. You don't need to bother about hosting the documentation anymore. The service, you developed, is turned self-documented. You can use standard .Net comments to add more information.

Once you have an API that can describe itself in Swagger, you've opened the treasure chest of Swagger-based tools including a client generator that can be targeted to a wide range of popular platforms. See swagger-codegen for more details.

Let's see an example.

In this line of code we place an action's description:

/// <summary>
/// Get a product by id
/// </summary>
/// <param name="id">A product id</param>
[ProducesResponseType(typeof(Dto.Product), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Dto.Product GetById(int id)
{
  // implementation
}

This is a documentation for a class Dto.Product:

    /// <summary>
    /// DTO for reading product (-s)
    /// </summary>

    public class Product
    {
        /// <summary>
        /// Product id
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Product name
        /// </summary>
        /// <example>lime</example>
        public string Name { get; set; }
    }

To see a result you should run your service and navigate to http://localhost:5000/swagger in your favorite browser. And magic appears :).

All aspects of Swashbuckle configuration gathered in Configuration/DependenciesConfig.cs and Configuration/MiddlewareConfig.cs files.