Reputation: 34028
I am trying to call an endpoint with post method.
The code is:
import React, { Component } from 'react';
import { Input} from 'antd';
import Form from '../../components/uielements/form';
import Button from '../../components/uielements/button';
import Notification from '../../components/notification';
import { adalApiFetch } from '../../adalConfig';
const FormItem = Form.Item;
class CreateSiteCollectionForm extends Component {
constructor(props) {
super(props);
this.state = {Alias:'',DisplayName:'', Description:''};
this.handleChangeAlias = this.handleChangeAlias.bind(this);
this.handleChangeDisplayName = this.handleChangeDisplayName.bind(this);
this.handleChangeDescription = this.handleChangeDescription.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChangeAlias(event){
this.setState({Alias: event.target.value});
}
handleChangeDisplayName(event){
this.setState({DisplayName: event.target.value});
}
handleChangeDescription(event){
this.setState({Description: event.target.value});
}
handleSubmit(e){
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
let data = new FormData();
//Append files to form data
//data.append(
const options = {
method: 'post',
body: JSON.stringify(
{
"Alias": this.state.Alias,
"DisplayName": this.state.DisplayName,
"Description": this.state.Description
}),
config: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
};
adalApiFetch(fetch, "/SiteCollections/CreateModernSite", options)
.then(response =>{
if(response.status === 204){
Notification(
'success',
'Site collection created',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Site collection not created',
error
);
console.error(error);
});
}
});
}
render() {
const { getFieldDecorator } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 14 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 14,
offset: 6,
},
},
};
return (
<Form onSubmit={this.handleSubmit}>
<FormItem {...formItemLayout} label="Alias" hasFeedback>
{getFieldDecorator('Alias', {
rules: [
{
required: true,
message: 'Please input your alias',
}
]
})(<Input name="alias" id="alias" onChange={this.handleChangeAlias} />)}
</FormItem>
<FormItem {...formItemLayout} label="Display Name" hasFeedback>
{getFieldDecorator('displayname', {
rules: [
{
required: true,
message: 'Please input your display name',
}
]
})(<Input name="displayname" id="displayname" onChange={this.handleChangedisplayname} />)}
</FormItem>
<FormItem {...formItemLayout} label="Description" hasFeedback>
{getFieldDecorator('description', {
rules: [
{
required: true,
message: 'Please input your description',
}
],
})(<Input name="description" id="description" onChange={this.handleChangeDescription} />)}
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">
Create modern site
</Button>
</FormItem>
</Form>
);
}
}
const WrappedCreateSiteCollectionForm = Form.create()(CreateSiteCollectionForm);
export default WrappedCreateSiteCollectionForm;
and the webapi is this one:
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using TenantManagementWebApi.Entities;
using TenantManagementWebApi.Factories;
using Cosmonaut.Extensions;
using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Sites;
using TenantManagementWebApi.Components;
namespace TenantManagementWebApi.Controllers
{
[Authorize]
public class SiteCollectionsController : ApiController
{
// GET: ModernTeamSite
public async Task<List<TenantManagementWebApi.Entities.SiteCollection>> Get()
{
var tenant = await TenantHelper.GetTenantAsync();
using (var cc = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret))
{
Tenant tenantOnline = new Tenant(cc);
SPOSitePropertiesEnumerable siteProps = tenantOnline.GetSitePropertiesFromSharePoint("0", true);
cc.Load(siteProps);
cc.ExecuteQuery();
List<TenantManagementWebApi.Entities.SiteCollection> sites = new List<TenantManagementWebApi.Entities.SiteCollection>();
foreach (var site in siteProps)
{
sites.Add(new TenantManagementWebApi.Entities.SiteCollection()
{
Url = site.Url,
Owner = site.Owner,
Template = site.Template,
Title = site.Title
});
}
return sites;
};
}
[HttpPost]
//[Route("api/SiteCollections/CreateModernSite")]
public async Task<string> CreateModernSite(string Alias, string DisplayName, string Description)
{
var tenant = await TenantHelper.GetTenantAsync();
using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret))
{
var teamContext = await context.CreateSiteAsync(
new TeamSiteCollectionCreationInformation
{
Alias = Alias, // Mandatory
DisplayName = DisplayName, // Mandatory
Description = Description, // Optional
//Classification = Classification, // Optional
//IsPublic = IsPublic, // Optional, default true
}
);
teamContext.Load(teamContext.Web, w => w.Url);
teamContext.ExecuteQueryRetry();
return teamContext.Web.Url;
}
}
}
}
Upvotes: 3
Views: 20024
Reputation: 247433
Attribute routing is enabled, according to the screenshot posted in comments, as the WebApiConfig
has the default configuration
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// Attribute routing.
config.MapHttpAttributeRoutes();
// Convention-based routing.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Note the api
prefix on the convention based route.
The request from client is being made to /SiteCollections/CreateModernSite
which wont match the Web API as the API controller appears to not be using the attribute routing and the requested URL does not match the Web API convention based route.
Also on the client side a JSON body is constructed in options while the content type is set to 'multipart/form-data'
If the intention is to POST content in the body, then on the server side you would need to make a few changes to make the API accessible.
[Authorize]
[RoutePrefix("api/SiteCollections")]
public class SiteCollectionsController : ApiController {
// GET api/SiteCollections
[HttpGet]
[Route("")]
public async Task<IHttpActionResult> Get() {
var tenant = await TenantHelper.GetTenantAsync();
using (var cc = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
var tenantOnline = new Tenant(cc);
SPOSitePropertiesEnumerable siteProps = tenantOnline.GetSitePropertiesFromSharePoint("0", true);
cc.Load(siteProps);
cc.ExecuteQuery();
var sites = siteProps.Select(site =>
new TenantManagementWebApi.Entities.SiteCollection() {
Url = site.Url,
Owner = site.Owner,
Template = site.Template,
Title = site.Title
})
.ToList();
return Ok(sites);
}
}
// POST api/SiteCollections
[HttpPost]
[Route("")]
public async Task<IHttpActionResult> CreateModernSite([FromBody]NewSiteInformation model) {
if(ModelState.IsValid) {
var tenant = await TenantHelper.GetTenantAsync();
using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
var teamContext = await context.CreateSiteAsync(
new TeamSiteCollectionCreationInformation {
Alias = model.Alias, // Mandatory
DisplayName = model.DisplayName, // Mandatory
Description = model.Description, // Optional
//Classification = Classification, // Optional
//IsPublic = IsPublic, // Optional, default true
}
);
teamContext.Load(teamContext.Web, _ => _.Url);
teamContext.ExecuteQueryRetry();
//204 with location and content set to created URL
return Created(teamContext.Web.Url, teamContext.Web.Url);
}
}
return BadRequest(ModelState);
}
public class NewSiteInformation {
[Required]
public string Alias { get; set; }
[Required]
public string DisplayName { get; set; }
public string Description { get; set; }
//...
}
}
Note the inclusion of a proper strongly typed object model for the POST action, model validation and the returning of the proper HTTP status code as expected by the client. (204)
On the client side, update the URL being called to match the API controller's route and update the options to send the correct content type.
//...
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
Alias: this.state.Alias,
DisplayName: this.state.DisplayName,
Description: this.state.Description
})
};
adalApiFetch(fetch, "api/SiteCollections", options)
.then(response =>{
if(response.status === 204){
Notification(
'success',
'Site collection created',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Site collection not created',
error
);
console.error(error);
});
//...
Note how the headers
are directly in the fetch options as apposed to config.headers
in the original code.
Upvotes: 2
Reputation: 1331
Try moving your Post request parameters into a class like this
public class TeamSiteInformation
{
public string Alias { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
}
and modify your method CreateModernSite signature to
[HttpPost]
public void CreateModernSite([FromBody]TeamSiteInformation site_info)
{
and in Change 'Content-Type': 'multipart/form-data' to 'application/json' in your reactjs app
Upvotes: 1