Web Service - Standalone

Content:

  1. Introduction
  2. Concept
  3. Java
  4. .NET
  5. Python
  6. PHP

Introduction:

This article describes Web Services running standalone aka as normal server processes outside of a web-server/application/server.

For the examples using RESTful web services it is a requirement to have read the stuff in Web Service - RESTful or have similar knowledge as this article will not explain the details of RESTful web services.

Concept:

Micro-services are in fashion in these years. The strong decoupling is attractive.

A key feature of web-servers/application-servers is the ability to run multiple web-applications/web-services.

If one want to truly decouple services then running each service in its own web-server/application-server is necessary.

But if one does that then one does not need all the overhead in web-servers/application-servers to manage multiple web-applications/web-services.

So it becomes desirable to run a simple standalone process exposing only a single micro-service.

Instead of:

Web Service Traditional

then:

Web Service Standalone

The following sections will show examples in various technologies.

The examples will provide the exact same API as used in Web Service - RESTful.

Java:

Standard Java EE deployment model looks like:

Web Service Java Traditional

There are a few options for standalone web services in Java:

Manual embedded Jetty or Tomcat:

The example will show embedded Jetty.

I believe Jetty is more common than Tomcat in such a setup.

Deployment model looks like:

Web Service Java Embedded

TestServer.java:

package demo;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;

import com.sun.jersey.spi.container.servlet.ServletContainer;

public class TestServer {
    private static final int PORT = 8081;
    private static final String CONTEXT = "/";
    private static final String API = "/testapi/*";
    public static void main(String[] args) throws Exception {
        Server server = new Server(PORT);
        ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        ctx.setContextPath(CONTEXT);
        server.setHandler(ctx);
        ServletHolder srvlet = ctx.addServlet(ServletContainer.class, API);
        srvlet.setInitOrder(1);
        srvlet.setInitParameter("com.sun.jersey.config.property.packages", "ws.rest");
        srvlet.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true");
        server.start();
        server.join();
    }
}

Most of the code is easy to read.

        srvlet.setInitParameter("com.sun.jersey.config.property.packages", "ws.rest");

Instruct the embedded servlet container to scan all classes in package ws.rest for JAX-RS annotations defining services.

        srvlet.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true");

Fixes a JSON serialization problem.

The web service URL becomes http://localhost:8081/testapi (for local access).

All the remaining code are exactly the same as when deploying a standard web application in a servlet container.

This example will be using JAX-RS.

T1.java:

package ws.rest;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class T1 {
    private int f1;
    private String f2;
    public T1() {
        this(0, "");
    }
    public T1(int f1, String f2) {
        this.f1 = f1;
        this.f2 = f2;
    }
    public int getF1() {
        return f1;
    }
    public void setF1(int f1) {
        this.f1 = f1;
    }
    public String getF2() {
        return f2;
    }
    public void setF2(String f2) {
        this.f2 = f2;
    }
}

T1Array.java:

package ws.rest;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="t1array")
public class T1Array {
    private T1[] t1array;
    public T1Array() {
        this(new T1[0]);
    }
    public T1Array(T1[] array) {
        this.t1array = array;
    }
    @XmlElements(@XmlElement(name="t1",type=T1.class))
    public T1[] getT1array(){
        return t1array;
    }
    public void setT1array(T1[] array) {
        this.t1array = array;
    }
}

TestService.java:

package ws.rest;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
////import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;

@Path("/t1")
public class TestService {
    @Context 
    private HttpServletResponse response;
    Test real = new Test();
    @GET
    @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
    @Path("/{f1}")
    public T1 getOne(@PathParam("f1") int f1) {
        ////response.setHeader("Access-Control-Allow-Origin", "*"); 
        return real.getOne(f1);
    }
    // there is no getAll as it is processed as getSome with default query parameters
    @GET
    @Produces({MediaType.APPLICATION_XML})
    @Path("")
    public T1Array getSomeXml(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
        response.setHeader("Access-Control-Allow-Origin", "*"); 
        if(start == 0 && finish == 2147483647) {
            return new T1Array(real.getAll());
        } else {
            return new T1Array(real.getSome(start, finish));
        }
    }
    @GET
    @Produces({MediaType.APPLICATION_JSON})
    @Path("")
    public T1[] getSomeJson(@DefaultValue("0") @QueryParam("start") int start, @DefaultValue("2147483647") @QueryParam("finish") int finish) {
        ////response.setHeader("Access-Control-Allow-Origin", "*"); 
        if(start == 0 && finish == 2147483647) {
            return real.getAll();
        } else {
            return real.getSome(start, finish);
        }
    }
    @POST
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
    @Produces({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
    @Path("")
    public T1 save(T1 o) {
        ////response.setHeader("Access-Control-Allow-Origin", "*"); 
        if(real.save(o)) {
            return o;
        } else {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }
    @PUT
    @Path("/{f1}")
    @Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})
    public void update(@PathParam("f1") int f1, T1 o) {
        ////response.setHeader("Access-Control-Allow-Origin", "*"); 
        if(o.getF1() == f1 && real.save(o)) {
        } else {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }
    @DELETE
    @Path("/{f1}")
    public void delete(@PathParam("f1") int f1) {
        ////response.setHeader("Access-Control-Allow-Origin", "*"); 
        if(real.delete(f1)) {
        } else {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }
    ////@OPTIONS
    ////@Path("")
    ////public void options1() {
    ////    response.setHeader("Access-Control-Allow-Origin", "*");
    ////    response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    ////    response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
    ////}
    ////@OPTIONS
    ////@Path("/{f1}")
    ////public void options2(@PathParam("f1") int f1) {
    ////    response.setHeader("Access-Control-Allow-Origin", "*");
    ////    response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    ////    response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept");
    ////}
}

Test.java:

package ws.rest;

public class Test {
    public T1 getOne(int f1) {
        // dummy implementation
        return new T1(f1, "getOne");
    }
    public T1[] getAll() {
        // dummy implementation
        T1[] res = new T1[3];
        res[0] = new T1(1, "getAll #1");
        res[1] = new T1(2, "getAll #2");
        res[2] = new T1(3, "getAll #3");
        return res;
    }
    public T1[] getSome(int start, int finish) {
        // dummy implementation
        T1[] res = new T1[finish - start + 1];
        for(int i = 0; i < res.length; i++) {
            res[i] = new T1(start + i, "getSome #" + (start + i));
        }
        return res;
    }
    public boolean save(T1 o) {
        // dummy implementation
        return o.getF1() > 0 && o.getF2().length() > 0;
    }
    public boolean delete(int f1) {
        // dummy implementation
        return f1 > 0;
    }
}

For details on what these do see Java JAX-RS here.

For the main program put Jetty jar files in classpath. For JAX-RS put Jersey or RestEasy jar files in classpath.

Spring Boot Framework with embedded Tomcat or Jetty:

Spring Boot has become very popular in recent years for this purpose.

The example will show embedded Tomcat.

I believe Tomcat is more common than Jetty with Spring Boot.

Deployment model looks like:

Web Service Java Spring Boot

TestServer.java:

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = {"ws.rest" })
public class TestServer {
    private static final String PORT = "8082";
    private static final String API = "/testapi";
    public static void main(String[] args) {
        System.setProperty("server.port", PORT);
        System.setProperty("server.servlet.context-path", API);
        SpringApplication.run(TestServer.class, args);
    }
}

Most of the code is easy to read.

@SpringBootApplication(scanBasePackages = {"ws.rest" })

Instruct Spring Boot that this is an application and to scan the package ws.rest for Spring MVC annotations defining services.

The web service URL becomes http://localhost:8082/testapi (for local access).

All the remaining code are exactly the same as when deploying a standard web application in a servlet container.

This example will be using Spring MVC.

T1.java:

package ws.rest;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class T1 {
    private int f1;
    private String f2;
    public T1() {
        this(0, "");
    }
    public T1(int f1, String f2) {
        this.f1 = f1;
        this.f2 = f2;
    }
    public int getF1() {
        return f1;
    }
    public void setF1(int f1) {
        this.f1 = f1;
    }
    public String getF2() {
        return f2;
    }
    public void setF2(String f2) {
        this.f2 = f2;
    }
}

T1Array.java:

package ws.rest;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="t1array")
public class T1Array {
    private T1[] t1array;
    public T1Array() {
        this(new T1[0]);
    }
    public T1Array(T1[] array) {
        this.t1array = array;
    }
    @XmlElements(@XmlElement(name="t1",type=T1.class))
    public T1[] getT1array(){
        return t1array;
    }
    public void setT1array(T1[] array) {
        this.t1array = array;
    }
}

TestService.java:

package ws.rest;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
////import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import ws.rest.T1;
import ws.rest.T1Array;
import ws.rest.Test;

////@CrossOrigin(origins="*",methods={RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE,RequestMethod.OPTIONS},allowedHeaders={"Content-Type","Accept"})
@RestController
@RequestMapping(value="/t1")
public class TestController {
    private Test real = new Test();
    @RequestMapping(value="/{f1}",method=RequestMethod.GET,produces={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<T1> getOne(@PathVariable("f1") int f1) {
        return new ResponseEntity<T1>(real.getOne(f1), HttpStatus.OK);
    }
    // there is no getAll as it is processed as getSome with default query parameters
    @RequestMapping(value="",method=RequestMethod.GET,produces={MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<T1Array> getSomeXml(@RequestParam(value="start",defaultValue="0") int start, @RequestParam(value="finish",defaultValue="2147483647") int finish) {
        if(start == 0 && finish == 2147483647) {
            return new ResponseEntity<T1Array>(new T1Array(real.getAll()), HttpStatus.OK);
        } else {
            return new ResponseEntity<T1Array>(new T1Array(real.getSome(start, finish)), HttpStatus.OK);
        }
    }
    @RequestMapping(value="",method=RequestMethod.GET,produces={MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<T1[]> getSomeJson(@RequestParam(value="start",defaultValue="0") int start, @RequestParam(value="finish",defaultValue="2147483647") int finish) {
        if(start == 0 && finish == 2147483647) {
            return new ResponseEntity<T1[]>(real.getAll(), HttpStatus.OK);
        } else {
            return new ResponseEntity<T1[]>(real.getSome(start, finish), HttpStatus.OK);
        }
    }
    @RequestMapping(value="",method=RequestMethod.POST,produces={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE},consumes={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<T1> save(@RequestBody T1 o) {
        if(real.save(o)) {
            return new ResponseEntity<T1>(o, HttpStatus.OK);
        } else {
            return new ResponseEntity<T1>(HttpStatus.NOT_FOUND);
        }
    }
    @RequestMapping(value="/{f1}",method=RequestMethod.PUT,consumes={MediaType.APPLICATION_JSON_VALUE,MediaType.APPLICATION_XML_VALUE})
    public ResponseEntity<Void> update(@PathVariable("f1") int f1, @RequestBody T1 o) {
        if(o.getF1() == f1 && real.save(o)) {
            return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
        }
    }
    @RequestMapping(value="/{f1}",method=RequestMethod.DELETE)
    public ResponseEntity<Void> delete(@PathVariable("f1") int f1) {
        if(real.delete(f1)) {
            return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
        }
    }
}

Test.java:

package ws.rest;

public class Test {
    public T1 getOne(int f1) {
        // dummy implementation
        return new T1(f1, "getOne");
    }
    public T1[] getAll() {
        // dummy implementation
        T1[] res = new T1[3];
        res[0] = new T1(1, "getAll #1");
        res[1] = new T1(2, "getAll #2");
        res[2] = new T1(3, "getAll #3");
        return res;
    }
    public T1[] getSome(int start, int finish) {
        // dummy implementation
        T1[] res = new T1[finish - start + 1];
        for(int i = 0; i < res.length; i++) {
            res[i] = new T1(start + i, "getSome #" + (start + i));
        }
        return res;
    }
    public boolean save(T1 o) {
        // dummy implementation
        return o.getF1() > 0 && o.getF2().length() > 0;
    }
    public boolean delete(int f1) {
        // dummy implementation
        return f1 > 0;
    }
}

For details on what these do see Java Spring MVC here.

Put Spring Boot, Spring MVC, Soring and Jackson jar files in classpath.

.NET:

Standard ASP.NET deployment model looks like:

Web Service ASP.NET Traditional

WCF self-hosting:

WCF supports both hosting in ASP.NET and self-hosting.

I believe that WCF self-hosting is very rare in practice, but it is possible.

Deployment model looks like:

Web Service WCF Self-Host

Program.cs:

using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Description;

using Ws.Rest;

namespace Demo
{
    public class Program
    {
        public static readonly Uri URL = new Uri("http://localhost:8083/testapi");
        public static void Main(string[] args)
        {
            // 
            using (ServiceHost host = new ServiceHost(typeof(TestService), URL))
            {
                host.Open();
                Console.ReadKey();
                host.Close();
            }
        }
    }
}

The code is easy to read.

The web service URL becomes http://localhost:8083/testapi (for local access).

For it to work the following command should be executed as administrator:

netsh http add urlacl url=http://+:8083/testapi user=ARNE

(replace ARNE with the username you will run it under)

All the remaining code are exactly the same as when deploying in ASP.NET.

TestService.cs:

using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace Ws.Rest
{
    [DataContract(Name="t1",Namespace="")]
    public class T1
    {
        [DataMember(Name="f1")]
        public int F1 { get; set; }
        [DataMember(Name="f2")]
        public string F2 { get; set; }
        public T1() : this(0, "")
        {
        }
        public T1(int f1, string f2)
        {
            this.F1 = f1;
            this.F2 = f2;
        }
    }
    [CollectionDataContract(Name="t1array",Namespace="")]
    public class T1Array : List<T1>
    {
        public T1Array() : this(new T1[0])
        {
        }
        public T1Array(T1[] array) : base(array)
        {
        }
    }
    public class Test
    {
        public T1 GetOne(int f1)
        {
            // dummy implementation
            return new T1(f1, "getOne");
        }
        public T1[] GetAll()
        {
            // dummy implementation
            T1[] res = new T1[3];
            res[0] = new T1(1, "getAll #1");
            res[1] = new T1(2, "getAll #2");
            res[2] = new T1(3, "getAll #3");
            return res;
        }
        public T1[] GetSome(int start, int finish)
        {
            // dummy implementation
            T1[] res = new T1[finish - start + 1];
            for(int i = 0; i < res.Length; i++)
            {
                res[i] = new T1(start + i, "getSome #" + (start + i));
            }
            return res;
        }
        public bool Save(T1 o)
        {
            // dummy implementation
            return o.F1 > 0 && o.F2.Length > 0;
        }
        public bool Delete(int f1)
        {
            // dummy implementation
            return f1 > 0;
        }
    }
    [ServiceContract]
    public interface ITestService
    {
        [OperationContract]
        [WebInvoke(UriTemplate="/t1/{f1}",Method="GET",BodyStyle=WebMessageBodyStyle.Bare)]
        T1 GetOne(string f1);
        // there is no GetAll as it is processed as GetSome with null query parameters
        [OperationContract]
        [WebInvoke(UriTemplate="/t1?start={start}&finish={finish}",Method="GET",RequestFormat=WebMessageFormat.Xml,BodyStyle=WebMessageBodyStyle.Bare)]
        T1Array GetSome(string start, string finish);
        [OperationContract]
        [WebInvoke(UriTemplate="/t1",Method="POST",BodyStyle=WebMessageBodyStyle.Bare)]
        T1 Save(T1 o);
        [OperationContract]
        [WebInvoke(UriTemplate="/t1/{f1}",Method="PUT")]
        void Update(string f1, T1 o);
        [OperationContract]
        [WebInvoke(UriTemplate="/t1/{f1}",Method="DELETE")]
        void Delete(string f1);
        ////[WebInvoke(UriTemplate="/t1",Method="OPTIONS")]
        ////void Option1();
        ////[WebInvoke(UriTemplate="/t1/{f1}",Method="OPTIONS")]
        ////void Option2(string f1);
    }
    [ServiceBehavior]
    public class TestService : ITestService
    {
        private Test real = new Test();
        public T1 GetOne(string f1)
        {
            ////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
            return real.GetOne(int.Parse(f1));
        }
        public T1Array GetSome(string start, string finish)
        {
            ////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
            if(start == null && finish == null)
            {
                return new T1Array(real.GetAll());
            }
            else
            {
                return new T1Array(real.GetSome(int.Parse(start), int.Parse(finish)));
            }
        }
        public T1 Save(T1 o)
        {
            ////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
            if(real.Save(o))
            {
                return o;    
            }
            else
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
                return null;
            }
        }
        public void Update(string f1, T1 o)
        {
            ////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
            if(int.Parse(f1) == o.F1 && real.Save(o))
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
            }
            else
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
            }
        }
        public void Delete(string f1)
        {
            ////WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
            if(real.Delete(int.Parse(f1)))
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
            }
            else
            {
                WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
            }
        }
        ////public void Option1()
        ////{
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
        ////    WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
        ////}
        ////public void Option2(string f1)
        ////{
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        ////    WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
        ////    WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NoContent;
        ////}
    }
}

app.config fragment:

    <system.serviceModel>
        <services>
            <service name="Ws.Rest.TestService">
                <endpoint binding="webHttpBinding" contract="Ws.Rest.ITestService" behaviorConfiguration="ws"/>
            </service>
        </services>
        <behaviors>
            <endpointBehaviors>
                <behavior name="ws">
                    <webHttp automaticFormatSelectionEnabled="true"/>
                </behavior>
            </endpointBehaviors>
        </behaviors>
    </system.serviceModel> 

For details on what these do see C# WCF here.

.NET Core:

.NET Core and ASP.NET Core are very similar but not totally identical to .NET and ASP.NET.

They have a different way to deploy web services.

Deployment model looks like:

Web Service ASP.NET Core

Program.cs:

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace Demo
{
    public class Startup
    {
        private const string PATH = "/testapi";
        public void ConfigureServices(IServiceCollection services)
        {
            ////services.AddCors(options => options.AddPolicy("AlmostAnythingGoes", builder => builder.AllowAnyOrigin().WithHeaders("Content-Type").WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")));
            services.AddMvc().AddXmlDataContractSerializerFormatters(); // XML support is not added by default - and AddXmlSerializerFormatters will add a formatter that ignores DataContract attributes
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UsePathBase(PATH);
            ////app.UseCors();
            app.UseMvc();
        }
    }
    public class Program
    {
        private const string URL = "http://localhost:8084";
        public static void Main(string[] args)
        {
             WebHost.CreateDefaultBuilder(args).UseUrls(URL).UseStartup<Startup>().Build().Run();
        }
    }
}

The code is easy to read. The only little trick is to add the correct XML formatter.

The web service URL becomes http://localhost:8084/testapi (for local access).

something.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" Version="2.1.1"/>
  </ItemGroup>
</Project>

Build command:

dotnet build

Run command:

dotnet run

My code look a little bit different from the code that Visual Studio generate, but fundamentaly it is the same.

All the remaining code are almost but not completely identical with the ASP.NET WebAPI code. Be careful.

(a few namespaces and a few class names have changed)

TestApi.cs:

using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.Serialization;
using System.Web.Http;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Ws.Rest
{
    [DataContract(Name="t1",Namespace="")]
    public class T1
    {
        [DataMember(Name="f1")]
        public int F1 { get; set; }
        [DataMember(Name="f2")]
        public string F2 { get; set; }
        public T1() : this(0, "")
        {
        }
        public T1(int f1, string f2)
        {
            this.F1 = f1;
            this.F2 = f2;
        }
    }
    [CollectionDataContract(Name="t1array",Namespace="")]
    public class T1Array : List<T1>
    {
        public T1Array() : this(new T1[0])
        {
        }
        public T1Array(T1[] array) : base(array)
        {
        }
    }
    public class Test
    {
        public T1 GetOne(int f1)
        {
            // dummy implementation
            return new T1(f1, "getOne");
        }
        public T1[] GetAll()
        {
            // dummy implementation
            T1[] res = new T1[3];
            res[0] = new T1(1, "getAll #1");
            res[1] = new T1(2, "getAll #2");
            res[2] = new T1(3, "getAll #3");
            return res;
        }
        public T1[] GetSome(int start, int finish)
        {
            // dummy implementation
            T1[] res = new T1[finish - start + 1];
            for(int i = 0; i < res.Length; i++)
            {
                res[i] = new T1(start + i, "getSome #" + (start + i));
            }
            return res;
        }
        public bool Save(T1 o)
        {
            // dummy implementation
            return o.F1 > 0 && o.F2.Length > 0;
        }
        public bool Delete(int f1)
        {
            // dummy implementation
            return f1 > 0;
        }
    }
    [Route("/t1")]
    ////[EnableCors("AlmostAnythingGoes")]
    [ApiController]
    public class TestApiController : ControllerBase
    {
        private Test real = new Test();
        [HttpGet("{f1}")]
        public IActionResult GetOne(int f1) 
        {
            return Ok(real.GetOne(f1));
        }
        [HttpGet("")]
        public IActionResult GetSome(int start = 0, int finish = 2147483647)
        {
            if(start == 0 && finish == 2147483647)
            {
                return Ok(new T1Array(real.GetAll()));
            }
            else
            {
                return Ok(new T1Array(real.GetSome(start, finish)));
            }
        }
        [HttpPost("")]
        public IActionResult Save(T1 o)
        {
            if(real.Save(o))
            {
                return Ok(o);    
            }
            else
            {
                return NotFound();
            }
        }
        [HttpPut("{f1}")]
        public IActionResult Update(int f1, T1 o)
        {
            if(f1 == o.F1 && real.Save(o))
            {
                return StatusCode(StatusCodes.Status204NoContent);
            }
            else
            {
                return NotFound();
            }
        }
        [HttpDelete("{f1}")]
        public IActionResult Delete(int f1)
        {
            if(real.Delete(f1))
            {
                return StatusCode(StatusCodes.Status204NoContent);
            }
            else
            {
                return NotFound();
            }
        }
    }
}

Minimal API:

.NET 5 inherited the .NET Core way , but since .NET 6 a simplified flavor called "Minimal API" has been available.

Program.cs:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
////builder.Services.AddCors(options => options.AddPolicy("AlmostAnythingGoes", builder => builder.AllowAnyOrigin().WithHeaders("Content-Type").WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")));
WebApplication app = builder.Build();
////app.UseCors();
Test real = new Test();
app.MapGet("/testapi/t1/{f1}", (int f1) => Results.Ok(real.GetOne(f1))); //// .RequireCors("AlmostAnythingGoes");
app.MapGet("/testapi/t1", (int start = 0, int finish = 2147483647) => Results.Ok((start == 0 && finish == 2147483647) ? real.GetAll() : real.GetSome(start, finish))); //// .RequireCors("AlmostAnythingGoes");
app.MapPost("/testapi/t1", (T1 o) => real.Save(o) ? Results.Ok(o) : Results.NotFound()); //// .RequireCors("AlmostAnythingGoes");
app.MapPut("/testapi/t1/{f1}", (int f1, T1 o) => (o.F1 == f1 && real.Save(o)) ? Results.NoContent() : Results.NotFound()); //// .RequireCors("AlmostAnythingGoes");
app.MapDelete("/testapi/t1/{f1}", (int f1) => real.Delete(f1) ? Results.NoContent() : Results.NotFound()); //// .RequireCors("AlmostAnythingGoes");
app.Run("http://localhost:8085");

Test.cs:

public class T1
{
    public int F1 { get; set; }
    public string F2 { get; set; }
    public T1() : this(0, "")
    {
    }
    public T1(int f1, string f2)
    {
        this.F1 = f1;
        this.F2 = f2;
    }
}

public class Test
{
    public T1 GetOne(int f1)
    {
        // dummy implementation
        return new T1(f1, "getOne");
    }
    public T1[] GetAll()
    {
        // dummy implementation
        T1[] res = new T1[3];
        res[0] = new T1(1, "getAll #1");
        res[1] = new T1(2, "getAll #2");
        res[2] = new T1(3, "getAll #3");
        return res;
    }
    public T1[] GetSome(int start, int finish)
    {
        // dummy implementation
        T1[] res = new T1[finish - start + 1];
        for(int i = 0; i < res.Length; i++)
        {
            res[i] = new T1(start + i, "getSome #" + (start + i));
        }
        return res;
    }
    public bool Save(T1 o)
    {
        // dummy implementation
        return o.F1 > 0 && o.F2.Length > 0;
    }
    public bool Delete(int f1)
    {
        // dummy implementation
        return f1 > 0;
    }
}

It can't get any simpler than that.

Note that it is only intended to produce JSON. Getting it to produce XML is a bit compliacted. So anyone wanting to supporting both XML and JSOn should not use minimal API.

Python:

There are several frameworks for creating web services in Python. One of the most widely used and most relevant are Flask.

Flask requires a newer Python. At the time of writing Python 3.8 or newer is required.

Flask can be used a few different ways.

Flask with custom serialization/deserialization:

This gives full control and can support both XML and JSON.

I suspect nobody does flask this way.

Modules to install via PIP: flask.

testapi.py:

from flask import Flask, request, Response
import json
import xml.dom.minidom
#from flask_cors import cross_origin

def Object2Xml(o):
    clz = o.__class__.__name__
    if clz == 'T1':
        return '<t1><f1>%d</f1><f2>%s</f2></t1>' % (o.f1, o.f2)
    if clz == 'T1Array':
        return '<t1array>' + ''.join(map(lambda o1 : Object2Xml(o1), o.array)) + '</t1array>'
    raise Exception('Unknown class: ' + clz)

def Object2Json(o):
    clz = o.__class__.__name__
    if clz == 'T1':
        return json.dumps(o.__dict__)
    if clz == 'T1Array':
        return '[' + ','.join(map(lambda o1 : Object2Json(o1), o.array)) + ']'
    raise Exception('Unknown class: ' + clz)

def Object2String(o, typ):
    if typ == 'application/xml':
        return Object2Xml(o)
    if typ == 'application/json':
        return Object2Json(o)
    else:
        raise Exception('Unknown type: ' + typ)

def Xml2Object(clz, s):
    doc = xml.dom.minidom.parseString(s)
    if clz == 'T1':
        return T1(int(doc.getElementsByTagName('f1')[0].firstChild.data), doc.getElementsByTagName('f2')[0].firstChild.data)
    if clz == 'T1Array':
        res = T1Array()
        for elm in doc.getElementsByTagName('t1'):
            res.array.append(T1(elm.getElementsByTagName('f1')[0].firstChild.data, elm.getElementsByTagName('f2')[0].firstChild.data))
        return res
    raise Exception('Unknown class: ' + clz)

def Json2Object(clz, s):
    o = json.loads(s)
    if clz == 'T1':
        return T1(o['f1'], o['f2'])
    if clz == 'T1Array':
        res = T1Array()
        for o1 in o:
            res.array.append(T1(o1['f1'], o1['f2']))
        return res
    raise Exception('Unknown class: ' + clz)

def String2Object(clz, s, typ):
    if typ == 'application/xml':
        return Xml2Object(clz, s)
    if typ == 'application/json':
        return Json2Object(clz, s)
    else:
        raise Exception('Unknown type: ' + typ)

class T1(object):
    def __init__(self, _f1 = 0, _f2 = ''):
        self.f1 = _f1
        self.f2 = _f2
    def __str__(self):
        return '{%s,%s}' % (self.f1, self.f2)

class T1Array(object):
    def __init__(self, init_array = []):
        self.array = init_array

class Test(object):
    def get_one(self, f1):
        return T1(f1, 'getOne')
    def get_all(self):
        return T1Array([ T1(1, 'getAll #1'), T1(2, 'getAll #2'), T1(3, 'getAll #3') ]) 
    def get_some(self, start, finish):
        res = []
        for i in range(finish - start + 1):
            res.append(T1(start + i, 'getSome #' + str(start + i)))
        return T1Array(res)
    def save(self, o):
        return o.f1 > 0 and len(o.f2) > 0
    def delete(self, f1):
        return f1 > 0

app = Flask(__name__)
real = Test()

@app.route('/testapi/t1/<int:f1>', methods=['GET'])
#@cross_origin()
def get_one(f1):
    typ = request.headers.get('Accept')
    return Response(Object2String(real.get_one(f1), typ), status=200, content_type=typ)

@app.route('/testapi/t1', methods=['GET'])
#@cross_origin()
def get_some():
    typ = request.headers.get('Accept')
    start = request.args.get('start', 0, type=int)
    finish = request.args.get('finish', 2147483647, type=int)
    if start == 0 and finish == 2147483647:
        return Response(Object2String(real.get_all(), typ), status=200, content_type=typ)
    else:
        return Response(Object2String(real.get_some(start, finish), typ), status=200, content_type=typ)

@app.route('/testapi/t1', methods=['POST'])
#@cross_origin()
def save():
    typ = request.headers.get('Accept')
    o = String2Object('T1', request.data, request.headers.get('Content-Type'))
    if real.save(o):
        return Response(Object2String(o, typ), status=200, content_type=typ)
    else:
        return Response(status=404)

@app.route('/testapi/t1/<int:f1>', methods=['PUT'])
#@cross_origin()
def update(f1):
    o = String2Object('T1', request.data, request.headers.get('Content-Type'))
    if o.f1 == f1 and real.save(o):
        return Response(status=204)
    else:
        return Response(status=404)

@app.route('/testapi/t1/<int:f1>', methods=['DELETE'])
#@cross_origin()
def delete(f1):
    if real.delete(f1):
        return Response(status=204)
    else:
        return Response(status=404)                                                                      
    
if __name__ == '__main__':
    app.run(host='localhost', port=8088)

Regular Flask:

If one can live with JSON only then flask can be used as intended with the jsonify module for serialization and deserialization.

This is how flask should be used.

Modules to install via PIP: flask and dataclasses.

testapi.py:

from flask import Flask, jsonify, request, Response
from dataclasses import dataclass
import json
#from flask_cors import cross_origin

@dataclass
class T1(object):
    f1: int
    f2: str
    def __init__(self, _f1 = 0, _f2 = ''):
        self.f1 = _f1
        self.f2 = _f2
    def __str__(self):
        return '{%s,%s}' % (self.f1, self.f2)

class Test(object):
    def get_one(self, f1):
        return T1(f1, 'getOne')
    def get_all(self):
        return [ T1(1, 'getAll #1'), T1(2, 'getAll #2'), T1(3, 'getAll #3') ] 
    def get_some(self, start, finish):
        res = []
        for i in range(finish - start + 1):
            res.append(T1(start + i, 'getSome #' + str(start + i)))
        return res
    def save(self, o):
        return o.f1 > 0 and len(o.f2) > 0
    def delete(self, f1):
        return f1 > 0

app = Flask(__name__)
real = Test()

@app.route('/testapi/t1/<int:f1>', methods=['GET'])
#@cross_origin()
def get_one(f1):
    return jsonify(real.get_one(f1))

@app.route('/testapi/t1', methods=['GET'])
#@cross_origin()
def get_some():
    start = request.args.get('start', 0, type=int)
    finish = request.args.get('finish', 2147483647, type=int)
    if start == 0 and finish == 2147483647:
        return jsonify(real.get_all())
    else:
        return jsonify(real.get_some(start, finish))

@app.route('/testapi/t1', methods=['POST'])
#@cross_origin()
def save():
    temp = request.json
    o = T1(temp['f1'], temp['f2'])
    if real.save(o):
        return jsonify(o)
    else:
        return Response(status=404)

@app.route('/testapi/t1/<int:f1>', methods=['PUT'])
#@cross_origin()
def update(f1):
    temp = request.json
    o = T1(temp['f1'], temp['f2'])
    if o.f1 == f1 and real.save(o):
        return Response(status=204)
    else:
        return Response(status=404)

@app.route('/testapi/t1/<int:f1>', methods=['DELETE'])
#@cross_origin()
def delete(f1):
    if real.delete(f1):
        return Response(status=204)
    else:
        return Response(status=404)                                                                      
    
if __name__ == '__main__':
    app.run(host='localhost', port=8086)

Flask with flask_restful:

Flask_restful is a module on top of flask that expose a more "resource like" API in the spirit of REST.

Modules to install via PIP: flask_restful, flask and dataclasses.

testapi.py:

from flask import Flask, jsonify, request, Response
from dataclasses import dataclass
import json
from flask_restful import Resource, Api 
#from flask_cors import CORS

@dataclass
class T1(object):
    f1: int
    f2: str
    def __init__(self, _f1 = 0, _f2 = ''):
        self.f1 = _f1
        self.f2 = _f2
    def __str__(self):
        return '{%s,%s}' % (self.f1, self.f2)

class Test(object):
    def get_one(self, f1):
        return T1(f1, 'getOne')
    def get_all(self):
        return [ T1(1, 'getAll #1'), T1(2, 'getAll #2'), T1(3, 'getAll #3') ] 
    def get_some(self, start, finish):
        res = []
        for i in range(finish - start + 1):
            res.append(T1(start + i, 'getSome #' + str(start + i)))
        return res
    def save(self, o):
        return o.f1 > 0 and len(o.f2) > 0
    def delete(self, f1):
        return f1 > 0

class T1Resource(Resource):
    def get(self, f1 = 0):
        if f1 > 0:
            return jsonify(real.get_one(f1))
        else:
            start = request.args.get('start', 0, type=int)
            finish = request.args.get('finish', 2147483647, type=int)
            if start == 0 and finish == 2147483647:
                return jsonify(real.get_all())
            else:
                return jsonify(real.get_some(start, finish))
    def post(self):
        temp = request.json
        o = T1(temp['f1'], temp['f2'])
        if real.save(o):
            return jsonify(o)
        else:
            return Response(status=404)
    def put(self, f1):
        temp = request.json
        o = T1(temp['f1'], temp['f2'])
        if o.f1 == f1 and real.save(o):
            return Response(status=204)
        else:
            return Response(status=404)
    def delete(self, f1):
        if real.delete(f1):
            return Response(status=204)
        else:
            return Response(status=404)                                                                      

app = Flask(__name__)
#CORS(app)
real = Test()
api = Api(app)
api.add_resource(T1Resource, '/testapi/t1', '/testapi/t1/<int:f1>') 
    
if __name__ == '__main__':
    app.run(host='localhost', port=8087)

PHP:

I am not aware of any good frameworks for PHP to do this.

One reason may be that there is less reason to do this for PHP as the footprint of a web-server with PHP is pretty small.

The option:

php -S http://localhost:8085

is for development only.

Article history:

Version Date Description
1.0 July 21st 2019 Initial version
1.1 November 21st 2023 Add section on .NET Minimal API
1.2 November 26th 2023 Add section on Python and flask

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj